lesiondiffusion-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 lesiondiffusion-mlx might be problematic. Click here for more details.

@@ -0,0 +1,541 @@
1
+ """Metal FP16 layers with the pinned Torch CPU oracle's reduction ordering.
2
+
3
+ The convolution and Welford algorithms follow PyTorch 2.13 CPU BlasKernel.cpp
4
+ and moments_utils.h. See root THIRD_PARTY_NOTICES.md. Inputs and outputs remain on Metal.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from functools import lru_cache
10
+ from typing import TYPE_CHECKING, cast
11
+
12
+ if TYPE_CHECKING:
13
+ from lesiondiffusion_mlx._typing import Array, MetalKernel, Mlx
14
+
15
+ from medmlx_core.layout import require_ncdhw
16
+
17
+ from lesiondiffusion_mlx._typing import ArrayConstructor
18
+
19
+ _CONV = r"""
20
+ uint tid = thread_position_in_threadgroup.x;
21
+ ulong row_base = ulong(threadgroup_position_in_grid.y) * 64;
22
+ uint channel_base = threadgroup_position_in_grid.z * 32;
23
+ const uint size = C * KD * KH * KW;
24
+ const uint complete = size - size % 4;
25
+ const ulong positions = ulong(OD) * OH * OW;
26
+ threadgroup float input_tile[64 * 32];
27
+ threadgroup float4 weight_tile[32 * 8];
28
+ uint row_slot = tid / 8;
29
+ uint channel_slot = tid % 8;
30
+ uint oc = channel_base + channel_slot * 4;
31
+ float4 a0 = 0, a1 = 0, a2 = 0, a3 = 0;
32
+ float4 b0 = 0, b1 = 0, b2 = 0, b3 = 0;
33
+ for (uint base = 0; base < complete; base += 32) {
34
+ for (uint i = tid; i < 64 * 32; i += 256) {
35
+ ulong row = row_base + i / 32;
36
+ uint k = base + i % 32;
37
+ float value = 0;
38
+ if (row < ulong(B) * positions && k < complete) {
39
+ uint c = k / (KD * KH * KW);
40
+ uint rem = k % (KD * KH * KW);
41
+ int z = int((row / (OH * OW)) % OD) * S + int(rem / (KH * KW)) - P;
42
+ int y = int((row / OW) % OH) * S + int((rem / KW) % KH) - P;
43
+ int x = int(row % OW) * S + int(rem % KW) - P;
44
+ if (z >= 0 && z < D && y >= 0 && y < H && x >= 0 && x < W) {
45
+ ulong n = row / positions;
46
+ value = float(values[(((n * C + c) * D + z) * H + y) * W + x]);
47
+ }
48
+ }
49
+ input_tile[i] = value;
50
+ }
51
+ uint k = base + tid / 8;
52
+ uint first = channel_base + (tid % 8) * 4;
53
+ float4 w = 0;
54
+ if (k < complete) {
55
+ for (uint j = 0; j < 4; ++j)
56
+ if (first + j < O) w[j] = float(weights[ulong(first + j) * size + k]);
57
+ }
58
+ weight_tile[tid] = w;
59
+ threadgroup_barrier(mem_flags::mem_threadgroup);
60
+ // Each partial visits precisely k % 4 in channel-major source order.
61
+ for (uint k = 0; k < 32; k += 4) {
62
+ float4 w0 = weight_tile[k * 8 + channel_slot];
63
+ float4 w1 = weight_tile[(k + 1) * 8 + channel_slot];
64
+ float4 w2 = weight_tile[(k + 2) * 8 + channel_slot];
65
+ float4 w3 = weight_tile[(k + 3) * 8 + channel_slot];
66
+ a0 += input_tile[row_slot * 32 + k] * w0;
67
+ a1 += input_tile[row_slot * 32 + k + 1] * w1;
68
+ a2 += input_tile[row_slot * 32 + k + 2] * w2;
69
+ a3 += input_tile[row_slot * 32 + k + 3] * w3;
70
+ b0 += input_tile[(row_slot + 32) * 32 + k] * w0;
71
+ b1 += input_tile[(row_slot + 32) * 32 + k + 1] * w1;
72
+ b2 += input_tile[(row_slot + 32) * 32 + k + 2] * w2;
73
+ b3 += input_tile[(row_slot + 32) * 32 + k + 3] * w3;
74
+ }
75
+ threadgroup_barrier(mem_flags::mem_threadgroup);
76
+ }
77
+ // The source appends every remaining tap to its first partial sum.
78
+ for (uint k = complete; k < size; ++k) {
79
+ float4 w = 0;
80
+ for (uint j = 0; j < 4; ++j)
81
+ if (oc + j < O) w[j] = float(weights[ulong(oc + j) * size + k]);
82
+ for (uint r = 0; r < 2; ++r) {
83
+ ulong row = row_base + row_slot + r * 32;
84
+ float value = 0;
85
+ if (row < ulong(B) * positions) {
86
+ uint c = k / (KD * KH * KW);
87
+ uint rem = k % (KD * KH * KW);
88
+ int z = int((row / (OH * OW)) % OD) * S + int(rem / (KH * KW)) - P;
89
+ int y = int((row / OW) % OH) * S + int((rem / KW) % KH) - P;
90
+ int x = int(row % OW) * S + int(rem % KW) - P;
91
+ if (z >= 0 && z < D && y >= 0 && y < H && x >= 0 && x < W) {
92
+ ulong n = row / positions;
93
+ value = float(values[(((n * C + c) * D + z) * H + y) * W + x]);
94
+ }
95
+ }
96
+ if (r == 0) a0 += value * w;
97
+ else b0 += value * w;
98
+ }
99
+ }
100
+ float4 total_a = ((a0 + a1) + a2) + a3;
101
+ float4 total_b = ((b0 + b1) + b2) + b3;
102
+ if constexpr (HAS_BIAS) {
103
+ float4 offset = 0;
104
+ for (uint j = 0; j < 4; ++j)
105
+ if (oc + j < O) offset[j] = float(bias[oc + j]);
106
+ total_a += offset;
107
+ total_b += offset;
108
+ }
109
+ for (uint r = 0; r < 2; ++r) {
110
+ ulong row = row_base + row_slot + r * 32;
111
+ if (row >= ulong(B) * positions) continue;
112
+ ulong n = row / positions;
113
+ ulong position = row % positions;
114
+ float4 total = r == 0 ? total_a : total_b;
115
+ for (uint j = 0; j < 4; ++j)
116
+ if (oc + j < O) result[(n * O + oc + j) * positions + position] = total[j];
117
+ }
118
+ """
119
+
120
+ _MOMENTS_HEADER = r"""
121
+ #pragma clang fp contract(off)
122
+ struct Moments { uint count; float4 mean; float4 m2; };
123
+ inline void merge(thread Moments& a, Moments b) {
124
+ uint count = a.count + b.count;
125
+ float ratio = count == 0 ? 0 : float(b.count) / float(count);
126
+ float4 delta = b.mean - a.mean;
127
+ float4 correction = ratio * delta;
128
+ float4 prior = delta * float(a.count);
129
+ a.mean = a.mean + correction;
130
+ a.m2 = metal::fma(prior, correction, a.m2 + b.m2);
131
+ a.count = count;
132
+ }
133
+ """
134
+
135
+ _PARTIAL_MOMENTS = r"""
136
+ uint index = thread_position_in_grid.x;
137
+ if (index >= B * G * CHUNKS) return;
138
+ uint group = index / CHUNKS;
139
+ uint chunk = index % CHUNKS;
140
+ uint vectors = SIZE / 8;
141
+ uint count = min(uint(16), vectors - chunk * 16);
142
+ float4 mean0 = 0, mean1 = 0, m20 = 0, m21 = 0;
143
+ for (uint j = 0; j < count; ++j) {
144
+ ulong base = ulong(group) * SIZE + (chunk * 16 + j) * 8;
145
+ float4 x0 = float4(values[base], values[base+1], values[base+2], values[base+3]);
146
+ float4 x1 = float4(values[base+4], values[base+5], values[base+6], values[base+7]);
147
+ float ratio = 1.0f / float(j + 1);
148
+ float4 delta0 = x0 - mean0;
149
+ float4 delta1 = x1 - mean1;
150
+ mean0 = metal::fma(delta0, ratio, mean0);
151
+ mean1 = metal::fma(delta1, ratio, mean1);
152
+ m20 = metal::fma(delta0, x0 - mean0, m20);
153
+ m21 = metal::fma(delta1, x1 - mean1, m21);
154
+ }
155
+ for (uint lane = 0; lane < 4; ++lane) {
156
+ partials[index * 16 + lane] = mean0[lane];
157
+ partials[index * 16 + 4 + lane] = m20[lane];
158
+ partials[index * 16 + 8 + lane] = mean1[lane];
159
+ partials[index * 16 + 12 + lane] = m21[lane];
160
+ }
161
+ """
162
+
163
+ _CASCADE = r"""
164
+ uint index = thread_position_in_grid.x;
165
+ if (index >= B * G * BLOCKS) return;
166
+ uint group = index / BLOCKS;
167
+ uint first = (index % BLOCKS) * BLOCK_CHUNKS;
168
+ uint vectors = SIZE / 8;
169
+ Moments stack[BLOCK_DEPTH];
170
+ for (uint level = 0; level < BLOCK_DEPTH; ++level)
171
+ stack[level] = {0, float4(0), float4(0)};
172
+ for (uint chunk = 0; chunk < BLOCK_CHUNKS; ++chunk) {
173
+ uint count = min(uint(16), vectors - (first + chunk) * 16);
174
+ ulong base = (ulong(group) * CHUNKS + first + chunk) * 16;
175
+ Moments lo = {count, float4(0), float4(0)};
176
+ Moments hi = {count, float4(0), float4(0)};
177
+ for (uint lane = 0; lane < 4; ++lane) {
178
+ lo.mean[lane] = partials[base + lane];
179
+ lo.m2[lane] = partials[base + 4 + lane];
180
+ hi.mean[lane] = partials[base + 8 + lane];
181
+ hi.m2[lane] = partials[base + 12 + lane];
182
+ }
183
+ merge(stack[0], lo);
184
+ merge(stack[0], hi);
185
+ uint mask = chunk + 1;
186
+ for (uint level = 1; level < BLOCK_DEPTH && (mask & 1) == 0; ++level) {
187
+ merge(stack[level], stack[level - 1]);
188
+ stack[level - 1] = {0, float4(0), float4(0)};
189
+ mask >>= 1;
190
+ }
191
+ }
192
+ // Stop before the final carry. Its empty/populated destination belongs to the
193
+ // complete group cascade and is merged by the next kernel in source order.
194
+ Moments result = stack[BLOCK_DEPTH - 1];
195
+ for (uint lane = 0; lane < 4; ++lane) {
196
+ cascades[index * 8 + lane] = result.mean[lane];
197
+ cascades[index * 8 + 4 + lane] = result.m2[lane];
198
+ }
199
+ """
200
+
201
+ _MOMENTS = r"""
202
+ uint group = thread_position_in_grid.x;
203
+ if (group >= B * G) return;
204
+ uint vectors = SIZE / 8;
205
+ Moments stack[DEPTH];
206
+ for (uint level = 0; level < DEPTH; ++level) stack[level] = {0, float4(0), float4(0)};
207
+ if constexpr (BLOCKS > 0) {
208
+ for (uint block = 0; block < BLOCKS; ++block) {
209
+ uint count = min(uint(BLOCK_CHUNKS * 16), vectors - block * BLOCK_CHUNKS * 16) * 2;
210
+ ulong base = (ulong(group) * BLOCKS + block) * 8;
211
+ Moments part = {count, float4(0), float4(0)};
212
+ for (uint lane = 0; lane < 4; ++lane) {
213
+ part.mean[lane] = cascades[base + lane];
214
+ part.m2[lane] = cascades[base + 4 + lane];
215
+ }
216
+ merge(stack[BLOCK_DEPTH], part);
217
+ uint mask = block + 1;
218
+ for (uint level = BLOCK_DEPTH + 1; level < DEPTH && (mask & 1) == 0; ++level) {
219
+ merge(stack[level], stack[level - 1]);
220
+ stack[level - 1] = {0, float4(0), float4(0)};
221
+ mask >>= 1;
222
+ }
223
+ }
224
+ }
225
+ for (uint chunk = BLOCKS * BLOCK_CHUNKS; chunk < CHUNKS; ++chunk) {
226
+ uint count = min(uint(16), vectors - chunk * 16);
227
+ ulong base = (ulong(group) * CHUNKS + chunk) * 16;
228
+ Moments lo = {count, float4(0), float4(0)};
229
+ Moments hi = {count, float4(0), float4(0)};
230
+ for (uint lane = 0; lane < 4; ++lane) {
231
+ lo.mean[lane] = partials[base + lane];
232
+ lo.m2[lane] = partials[base + 4 + lane];
233
+ hi.mean[lane] = partials[base + 8 + lane];
234
+ hi.m2[lane] = partials[base + 12 + lane];
235
+ }
236
+ merge(stack[0], lo);
237
+ merge(stack[0], hi);
238
+ uint mask = chunk + 1;
239
+ for (uint level = 1; level < DEPTH && (mask & 1) == 0; ++level) {
240
+ merge(stack[level], stack[level - 1]);
241
+ stack[level - 1] = {0, float4(0), float4(0)};
242
+ mask >>= 1;
243
+ }
244
+ }
245
+ for (uint level = 1; level < DEPTH; ++level) merge(stack[0], stack[level]);
246
+ uint count = 0;
247
+ float mean = 0, m2 = 0;
248
+ for (uint i = vectors * 8; i < SIZE; ++i) {
249
+ float x = float(values[ulong(group) * SIZE + i]);
250
+ float delta = x - mean;
251
+ ++count;
252
+ mean += delta / float(count);
253
+ m2 = metal::fma(delta, x - mean, m2);
254
+ }
255
+ uint added = vectors * 2;
256
+ for (uint lane = 0; lane < 4; ++lane) {
257
+ uint n = count + added;
258
+ float ratio = n == 0 ? 0 : float(added) / float(n);
259
+ float delta = stack[0].mean[lane] - mean;
260
+ mean = metal::fma(ratio, delta, mean);
261
+ m2 += metal::fma(delta * delta * ratio, float(count), stack[0].m2[lane]);
262
+ count = n;
263
+ }
264
+ """
265
+
266
+ _NORM = r"""
267
+ uint x = thread_position_in_grid.x;
268
+ uint y = thread_position_in_grid.y;
269
+ uint slice = thread_position_in_grid.z;
270
+ if (x >= W || y >= H || ulong(slice) >= ulong(B) * C * D) return;
271
+ ulong index = (ulong(slice) * H + y) * W + x;
272
+ uint group = uint(index / ulong(SIZE));
273
+ uint channel = (slice / D) % C;
274
+ float mean = stats[group * 2];
275
+ float var = max(stats[group * 2 + 1], 0.0f);
276
+ // Compensate the reciprocal root before rounding: Torch adds epsilon and
277
+ // takes the root in double, then stores the FP32 reciprocal.
278
+ float rstd = metal::precise::rsqrt(var + epsilon[0]);
279
+ float square = rstd * rstd;
280
+ float square_error = metal::fma(rstd, rstd, -square);
281
+ float residual = metal::fma(-var, square, 1.0f) - var * square_error - epsilon[0] * square;
282
+ rstd = metal::fma(0.5f * rstd, residual, rstd);
283
+ float scale = rstd * float(weight[channel]);
284
+ float offset = metal::fma(-scale, mean, float(bias[channel]));
285
+ output[index] = scale * float(values[index]) + offset;
286
+ """
287
+
288
+
289
+ _NORM_PARAMETERS = r"""
290
+ // Append to the source-ordered moments kernel. Its mean and variance are FP32,
291
+ // exactly as when written to/read from the standalone statistics buffer.
292
+ float var = max(m2 / float(SIZE), 0.0f);
293
+ // Retain _NORM's compensated reciprocal root, once per group.
294
+ float rstd = metal::precise::rsqrt(var + epsilon[0]);
295
+ float square = rstd * rstd;
296
+ float square_error = metal::fma(rstd, rstd, -square);
297
+ float residual = metal::fma(-var, square, 1.0f) - var * square_error - epsilon[0] * square;
298
+ rstd = metal::fma(0.5f * rstd, residual, rstd);
299
+ for (uint c = 0; c < C / G; ++c) {
300
+ uint channel = group * (C / G) + c;
301
+ float scale = rstd * float(weight[channel % C]);
302
+ float offset = metal::fma(-scale, mean, float(bias[channel % C]));
303
+ affine[channel * 2] = scale;
304
+ affine[channel * 2 + 1] = offset;
305
+ }
306
+ """
307
+
308
+ _NORM_VECTOR = r"""
309
+ uint vector = thread_position_in_grid.x;
310
+ uint channel = thread_position_in_grid.y;
311
+ if (ulong(vector) * 4 >= SPATIAL || channel >= B * C) return;
312
+ ulong base = ulong(channel) * SPATIAL + ulong(vector) * 4;
313
+ float scale = affine[channel * 2];
314
+ float offset = affine[channel * 2 + 1];
315
+ // SPATIAL is divisible by four, so each aligned load stays in its channel.
316
+ half4 value = *reinterpret_cast<const device half4*>(values + base);
317
+ float4 result = scale * float4(value) + offset;
318
+ *reinterpret_cast<device half4*>(output + base) = half4(result);
319
+ """
320
+
321
+
322
+ @lru_cache(maxsize=1)
323
+ def _norm_affine_kernels(mx: Mlx) -> tuple[MetalKernel, MetalKernel]:
324
+ """Fuse group moments with channel affines; apply without contracting multiply/add."""
325
+ return (
326
+ mx.fast.metal_kernel(
327
+ name="lesion_source_half_moments_affine",
328
+ input_names=["values", "partials", "cascades", "weight", "bias", "epsilon"],
329
+ output_names=["affine"],
330
+ source=_MOMENTS + _NORM_PARAMETERS,
331
+ header=_MOMENTS_HEADER,
332
+ ),
333
+ mx.fast.metal_kernel(
334
+ name="lesion_source_half_norm_vector",
335
+ input_names=["values", "affine"],
336
+ output_names=["output"],
337
+ source=_NORM_VECTOR,
338
+ header="#pragma clang fp contract(off)\n",
339
+ ),
340
+ )
341
+
342
+
343
+ @lru_cache(maxsize=1)
344
+ def _kernels(mx: Mlx) -> tuple[MetalKernel, MetalKernel, MetalKernel, MetalKernel, MetalKernel]:
345
+ def kernel(
346
+ name: str, inputs: list[str], output: str, source: str, header: str = ""
347
+ ) -> MetalKernel:
348
+ return mx.fast.metal_kernel(
349
+ name="lesion_source_half_" + name,
350
+ input_names=inputs,
351
+ output_names=[output],
352
+ header=header,
353
+ source=source,
354
+ ensure_row_contiguous=True,
355
+ )
356
+
357
+ return (
358
+ kernel("conv3d_tiled", ["values", "weights", "bias"], "result", _CONV),
359
+ kernel("partial_moments", ["values"], "partials", _PARTIAL_MOMENTS, _MOMENTS_HEADER),
360
+ kernel("cascade", ["partials"], "cascades", _CASCADE, _MOMENTS_HEADER),
361
+ kernel(
362
+ "moments",
363
+ ["values", "partials", "cascades"],
364
+ "stats",
365
+ _MOMENTS + "stats[group * 2] = mean;\nstats[group * 2 + 1] = m2 / float(SIZE);\n",
366
+ _MOMENTS_HEADER,
367
+ ),
368
+ kernel(
369
+ "norm",
370
+ ["values", "stats", "weight", "bias", "epsilon"],
371
+ "output",
372
+ _NORM,
373
+ "#pragma clang fp contract(off)\n",
374
+ ),
375
+ )
376
+
377
+
378
+ def conv3d_half(
379
+ x: Array, weight: Array, bias: Array | None, *, padding: int, stride: int, mx: Mlx
380
+ ) -> Array:
381
+ """Accumulate four channel-major partial sums, add bias, then round once.
382
+
383
+ Args:
384
+ x: Input ``(B, C, D, H, W)``; float16 in the runner.
385
+ weight: Kernel ``(O, C, KD, KH, KW)``.
386
+ bias: Per-output-channel bias ``(O,)``, or ``None``.
387
+ padding: Symmetric zero padding applied to all three spatial axes.
388
+ stride: Stride applied to all three spatial axes.
389
+ mx: The imported ``mlx.core`` module.
390
+
391
+ Returns:
392
+ Output ``(B, O, OD, OH, OW)`` in ``x``'s dtype.
393
+
394
+ Raises:
395
+ ValueError: If the channel counts or bias shape disagree, or the kernel is
396
+ larger than the padded input.
397
+ """
398
+ require_ncdhw(x, name="conv3d input")
399
+ batch, channels, depth, height, width = x.shape
400
+ outputs, inputs, kd, kh, kw = weight.shape
401
+ if inputs != channels:
402
+ raise ValueError("conv3d input channels differ from source weights")
403
+ if bias is not None and bias.shape != (outputs,):
404
+ raise ValueError("conv3d bias must have one value per output channel")
405
+ od, oh, ow = (
406
+ (d + 2 * padding - k) // stride + 1
407
+ for d, k in zip((depth, height, width), (kd, kh, kw), strict=True)
408
+ )
409
+ if min(od, oh, ow) < 1:
410
+ raise ValueError("conv3d kernel exceeds the padded input")
411
+ shape = (batch, outputs, od, oh, ow)
412
+ dimensions = dict(
413
+ B=batch,
414
+ C=channels,
415
+ D=depth,
416
+ H=height,
417
+ W=width,
418
+ O=outputs,
419
+ KD=kd,
420
+ KH=kh,
421
+ KW=kw,
422
+ OD=od,
423
+ OH=oh,
424
+ OW=ow,
425
+ S=stride,
426
+ P=padding,
427
+ HAS_BIAS=bias is not None,
428
+ )
429
+ return _kernels(mx)[0](
430
+ inputs=[x, weight, bias if bias is not None else mx.zeros((1,), dtype=x.dtype)],
431
+ template=list(dimensions.items()),
432
+ grid=(256, (batch * od * oh * ow + 63) // 64, (outputs + 31) // 32),
433
+ threadgroup=(256, 1, 1),
434
+ output_shapes=[shape],
435
+ output_dtypes=[x.dtype],
436
+ )[0]
437
+
438
+
439
+ def group_norm_half(
440
+ x: Array, weight: Array, bias: Array, *, groups: int, eps: float, mx: Mlx
441
+ ) -> Array:
442
+ """Cascade source-ordered Welford moments and apply the FP32 affine.
443
+
444
+ Args:
445
+ x: Input ``(B, C, D, H, W)``; float16 in the runner.
446
+ weight: Per-channel scale ``(C,)``.
447
+ bias: Per-channel shift ``(C,)``.
448
+ groups: Number of normalization groups; must divide ``C``.
449
+ eps: Variance epsilon.
450
+ mx: The imported ``mlx.core`` module.
451
+
452
+ Returns:
453
+ Normalized array with ``x``'s shape and dtype.
454
+
455
+ Raises:
456
+ ValueError: If ``C`` is not divisible by ``groups`` or the affine shapes
457
+ differ from ``(C,)``.
458
+ """
459
+ require_ncdhw(x, name="group_norm input")
460
+ batch, channels, d, h, w = x.shape
461
+ if channels % groups:
462
+ raise ValueError(f"group_norm channels {channels} must be divisible by num_groups {groups}")
463
+ if weight.shape != (channels,) or bias.shape != (channels,):
464
+ raise ValueError("group_norm affine must have one value per channel")
465
+ # Each Metal kernel otherwise makes its own contiguous copy of a strided
466
+ # activation. Share one graph-visible copy across moments and affine passes.
467
+ x = mx.contiguous(x)
468
+ positions = d * h * w
469
+ size = channels // groups * positions
470
+ chunks = max(1, (size // 8 + 15) // 16)
471
+ depth = max(1, (chunks - 1).bit_length())
472
+ # Power-of-two blocks replay complete low-level source cascades in parallel.
473
+ # The final carry and residual fold retain the original group-wide order.
474
+ block_depth = 7
475
+ block_chunks = 1 << block_depth
476
+ blocks = chunks // block_chunks if chunks > block_chunks else 0
477
+ _, partial_kernel, cascade_kernel, moments_kernel, norm_kernel = _kernels(mx)
478
+ dimensions = [("B", batch), ("G", groups), ("SIZE", size), ("CHUNKS", chunks)]
479
+ partials = partial_kernel(
480
+ inputs=[x],
481
+ template=dimensions,
482
+ grid=(batch * groups * chunks, 1, 1),
483
+ threadgroup=(128, 1, 1),
484
+ output_shapes=[(batch * groups * chunks, 16)],
485
+ output_dtypes=[mx.float32],
486
+ )[0]
487
+ cascade_dimensions = [
488
+ *dimensions,
489
+ ("BLOCKS", blocks),
490
+ ("BLOCK_CHUNKS", block_chunks),
491
+ ("BLOCK_DEPTH", block_depth),
492
+ ]
493
+ cascades = (
494
+ cascade_kernel(
495
+ inputs=[partials],
496
+ template=cascade_dimensions,
497
+ grid=(batch * groups * blocks, 1, 1),
498
+ threadgroup=(128, 1, 1),
499
+ output_shapes=[(batch * groups * blocks, 8)],
500
+ output_dtypes=[mx.float32],
501
+ )[0]
502
+ if blocks
503
+ else mx.zeros((1,), dtype=mx.float32)
504
+ )
505
+ epsilon = cast(ArrayConstructor, mx.array)([eps], dtype=mx.float32)
506
+ if x.dtype == mx.float16 and positions >= 4096 and positions % 4 == 0:
507
+ # Large aligned volumes use a vector copy with channel affines prepared
508
+ # by the moments kernel. Small/tail shapes retain the scalar path.
509
+ parameters_kernel, vector_kernel = _norm_affine_kernels(mx)
510
+ affine = parameters_kernel(
511
+ inputs=[x, partials, cascades, weight, bias, epsilon],
512
+ template=[*cascade_dimensions, ("DEPTH", depth), ("C", channels)],
513
+ grid=(batch * groups, 1, 1),
514
+ threadgroup=(32, 1, 1),
515
+ output_shapes=[(batch * channels, 2)],
516
+ output_dtypes=[mx.float32],
517
+ )[0]
518
+ return vector_kernel(
519
+ inputs=[x, affine],
520
+ template=[("B", batch), ("C", channels), ("SPATIAL", positions)],
521
+ grid=(positions // 4, batch * channels, 1),
522
+ threadgroup=(256, 1, 1),
523
+ output_shapes=[x.shape],
524
+ output_dtypes=[x.dtype],
525
+ )[0]
526
+ stats = moments_kernel(
527
+ inputs=[x, partials, cascades],
528
+ template=[*cascade_dimensions, ("DEPTH", depth)],
529
+ grid=(batch * groups, 1, 1),
530
+ threadgroup=(32, 1, 1),
531
+ output_shapes=[(batch * groups, 2)],
532
+ output_dtypes=[mx.float32],
533
+ )[0]
534
+ return norm_kernel(
535
+ inputs=[x, stats, weight, bias, epsilon],
536
+ template=[("B", batch), ("C", channels), ("SIZE", size), ("D", d), ("H", h), ("W", w)],
537
+ grid=(w, h, batch * channels * d),
538
+ threadgroup=(16, 4, 4),
539
+ output_shapes=[x.shape],
540
+ output_dtypes=[x.dtype],
541
+ )[0]
@@ -0,0 +1,57 @@
1
+ """Metal spatial copies used by the source model's factor-two upsampling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from lesiondiffusion_mlx._typing import Array, MetalKernel, Mlx
10
+
11
+
12
+ _UPSAMPLE = r"""
13
+ uint x = thread_position_in_grid.x;
14
+ uint y = thread_position_in_grid.y;
15
+ uint slice = thread_position_in_grid.z;
16
+ if (x >= OW || y >= OH || ulong(slice) >= ulong(B) * C * OD) return;
17
+ uint z = slice % OD;
18
+ uint c = (slice / OD) % C;
19
+ uint n = slice / (OD * C);
20
+ // Signed offsets retain reversed views; the input need not be row contiguous.
21
+ long loc = long(n) * values_strides[0] + long(c) * values_strides[1]
22
+ + long(z / 2) * values_strides[2] + long(y / 2) * values_strides[3]
23
+ + long(x / 2) * values_strides[4];
24
+ ulong dst = (ulong(slice) * OH + y) * OW + x;
25
+ output[dst] = values[loc];
26
+ """
27
+
28
+
29
+ @lru_cache(maxsize=1)
30
+ def _upsample_kernel(mx: Mlx) -> MetalKernel:
31
+ """Reuse the kernel object so repeated denoising does not create Metal libraries."""
32
+ return mx.fast.metal_kernel(
33
+ name="lesion_nearest_upsample2",
34
+ input_names=["values"],
35
+ output_names=["output"],
36
+ source=_UPSAMPLE,
37
+ ensure_row_contiguous=False,
38
+ )
39
+
40
+
41
+ def upsample_nearest2(x: Array, *, mx: Mlx) -> Array:
42
+ """Duplicate NCDHW voxels twice along each spatial axis in one copy pass.
43
+
44
+ The caller supplies a nonempty five-dimensional input. Integer division
45
+ implements the source's floor(i / 2) rule exactly. Input strides are read
46
+ directly; output is contiguous NCDHW with the input dtype and values.
47
+ """
48
+ batch, channels, d, h, w = x.shape
49
+ od, oh, ow = 2 * d, 2 * h, 2 * w
50
+ return _upsample_kernel(mx)(
51
+ inputs=[x],
52
+ template=[("B", batch), ("C", channels), ("OD", od), ("OH", oh), ("OW", ow)],
53
+ grid=(ow, oh, batch * channels * od),
54
+ threadgroup=(32, 4, 2),
55
+ output_shapes=[(batch, channels, od, oh, ow)],
56
+ output_dtypes=[x.dtype],
57
+ )[0]