lesiondiffusion-mlx 0.2.2__tar.gz → 0.2.4__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.
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/PKG-INFO +11 -32
- lesiondiffusion_mlx-0.2.4/README.md +46 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/pyproject.toml +5 -9
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/__init__.py +2 -5
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_attention.py +363 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_group_norm.py +218 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_nax_conv.py +591 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_nifti.py +111 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_reference.py +58 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_schedules.py +1 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_source_half.py +2 -0
- lesiondiffusion_mlx-0.2.4/src/lesiondiffusion_mlx/_typing.py +178 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/medmlx.py +654 -239
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_model.py +269 -43
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_pipeline.py +4 -1
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/mlx_sampling.py +5 -2
- lesiondiffusion_mlx-0.2.2/README.md +0 -66
- lesiondiffusion_mlx-0.2.2/src/lesiondiffusion_mlx/_nifti.py +0 -73
- lesiondiffusion_mlx-0.2.2/src/lesiondiffusion_mlx/_typing.py +0 -110
- lesiondiffusion_mlx-0.2.2/src/lesiondiffusion_mlx/mlx_weights.py +0 -216
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/.gitignore +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/LICENSE +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/THIRD_PARTY_NOTICES.md +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_arrays.py +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/_spatial.py +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/py.typed +0 -0
- {lesiondiffusion_mlx-0.2.2 → lesiondiffusion_mlx-0.2.4}/src/lesiondiffusion_mlx/reference.json +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lesiondiffusion-mlx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Native MLX implementation of LesionDiffusion
|
|
5
|
+
Project-URL: Homepage, https://huggingface.co/MedMLX/lesiondiffusion-mlx
|
|
5
6
|
Author: Joseph Sandoval, Alfredo Sandoval Jr.
|
|
6
7
|
License-Expression: Apache-2.0 AND MIT AND BSD-3-Clause
|
|
7
8
|
License-File: LICENSE
|
|
8
9
|
License-File: THIRD_PARTY_NOTICES.md
|
|
9
10
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
10
11
|
Requires-Python: <3.15,>=3.13
|
|
11
|
-
Requires-Dist: medmlx-core==0.2.
|
|
12
|
+
Requires-Dist: medmlx-core==0.2.5
|
|
12
13
|
Requires-Dist: nibabel<6,>=5.3
|
|
13
14
|
Requires-Dist: numpy<3,>=2.2
|
|
14
15
|
Requires-Dist: safetensors<1,>=0.6
|
|
15
16
|
Requires-Dist: scipy<2,>=1.15
|
|
16
17
|
Requires-Dist: tokenizers<1,>=0.21
|
|
17
|
-
Provides-Extra: conversion
|
|
18
|
-
Requires-Dist: torch<3,>=2.13; extra == 'conversion'
|
|
19
18
|
Provides-Extra: dev
|
|
20
19
|
Requires-Dist: einops<0.9,>=0.8.1; extra == 'dev'
|
|
21
20
|
Requires-Dist: hypothesis<7,>=6.100; extra == 'dev'
|
|
@@ -35,11 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
35
34
|
|
|
36
35
|
Add synthetic lesions to CT with native MLX, with controls for organ, size, placement and phenotype.
|
|
37
36
|
|
|
38
|
-
[
|
|
39
|
-
|
|
40
|
-

|
|
41
|
-
|
|
42
|
-
*Liver CT before and after lesion generation; the contour marks the generated lesion.*
|
|
37
|
+
[Model weights](https://huggingface.co/MedMLX/lesiondiffusion-mlx)
|
|
43
38
|
|
|
44
39
|
| Input | Output |
|
|
45
40
|
| :--- | :--- |
|
|
@@ -52,26 +47,7 @@ Add synthetic lesions to CT with native MLX, with controls for organ, size, plac
|
|
|
52
47
|
```bash
|
|
53
48
|
uv venv --python 3.13
|
|
54
49
|
source .venv/bin/activate
|
|
55
|
-
|
|
56
|
-
uv pip install \
|
|
57
|
-
"lesiondiffusion-mlx[conversion] @ git+$repo" \
|
|
58
|
-
huggingface-hub
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Get `LMNet.ckpt`, `LINet.ckpt` and `VQ.ckpt` from the [upstream weights folder](https://drive.google.com/drive/folders/1n6_eAWhsHBFBYtDxPtp_FXp_tNHhOCHi). Convert once, then generate:
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
from pathlib import Path
|
|
65
|
-
|
|
66
|
-
from huggingface_hub import hf_hub_download
|
|
67
|
-
from lesiondiffusion_mlx import write_conversion
|
|
68
|
-
|
|
69
|
-
tokenizer = hf_hub_download("microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224", "tokenizer.json")
|
|
70
|
-
write_conversion(
|
|
71
|
-
checkpoints={"lmnet": Path("LMNet.ckpt"), "linet": Path("LINet.ckpt"), "vq": Path("VQ.ckpt")},
|
|
72
|
-
tokenizer_path=Path(tokenizer),
|
|
73
|
-
output_dir=Path("lesiondiffusion-weights"),
|
|
74
|
-
)
|
|
50
|
+
uv pip install "lesiondiffusion-mlx>=0.2.4"
|
|
75
51
|
```
|
|
76
52
|
|
|
77
53
|
```python
|
|
@@ -80,7 +56,6 @@ from lesiondiffusion_mlx.medmlx import run
|
|
|
80
56
|
result = run(
|
|
81
57
|
image_path="ct_1mm_ras.nii.gz",
|
|
82
58
|
organ_mask_path="liver_mask.nii.gz",
|
|
83
|
-
weights_path="lesiondiffusion-weights",
|
|
84
59
|
output_dir="out",
|
|
85
60
|
organ="liver",
|
|
86
61
|
lesion_type="tumor",
|
|
@@ -90,10 +65,14 @@ result = run(
|
|
|
90
65
|
print(result["outputs"]["synthetic_image"])
|
|
91
66
|
```
|
|
92
67
|
|
|
93
|
-
|
|
68
|
+
Weights download from `MedMLX/lesiondiffusion-mlx` on first use. A local native
|
|
69
|
+
folder can also be passed as `weights_path`; it needs `model.safetensors` and
|
|
70
|
+
`tokenizer.json`. Conversion receipts are not required.
|
|
71
|
+
|
|
72
|
+
Version 0.2.3 accepts `lesion_mask_path` instead of `bbox_size_mm` to inpaint a supplied lesion shape. Inputs must support the documented crop sizes. Outputs use a RAS 1 mm grid; voxels outside the lesion can change. Synthetic output is not ground truth.
|
|
94
73
|
|
|
95
74
|
## License
|
|
96
75
|
|
|
97
|
-
|
|
76
|
+
Apache-2.0; license and third-party notices are included in the distribution. Model weights retain their publisher terms.
|
|
98
77
|
|
|
99
78
|
For research and education. Outputs require human review.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# lesiondiffusion-mlx
|
|
2
|
+
|
|
3
|
+
Add synthetic lesions to CT with native MLX, with controls for organ, size, placement and phenotype.
|
|
4
|
+
|
|
5
|
+
[Model weights](https://huggingface.co/MedMLX/lesiondiffusion-mlx)
|
|
6
|
+
|
|
7
|
+
| Input | Output |
|
|
8
|
+
| :--- | :--- |
|
|
9
|
+
| CT + aligned binary organ mask | Synthetic CT and lesion mask |
|
|
10
|
+
|
|
11
|
+
**Apple Silicon · Metal · Python 3.13 / 3.14** · [uv](https://docs.astral.sh/uv/)
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv venv --python 3.13
|
|
17
|
+
source .venv/bin/activate
|
|
18
|
+
uv pip install "lesiondiffusion-mlx>=0.2.4"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from lesiondiffusion_mlx.medmlx import run
|
|
23
|
+
|
|
24
|
+
result = run(
|
|
25
|
+
image_path="ct_1mm_ras.nii.gz",
|
|
26
|
+
organ_mask_path="liver_mask.nii.gz",
|
|
27
|
+
output_dir="out",
|
|
28
|
+
organ="liver",
|
|
29
|
+
lesion_type="tumor",
|
|
30
|
+
bbox_size_mm=[30, 30, 20],
|
|
31
|
+
placement="inside_parenchyma",
|
|
32
|
+
)
|
|
33
|
+
print(result["outputs"]["synthetic_image"])
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Weights download from `MedMLX/lesiondiffusion-mlx` on first use. A local native
|
|
37
|
+
folder can also be passed as `weights_path`; it needs `model.safetensors` and
|
|
38
|
+
`tokenizer.json`. Conversion receipts are not required.
|
|
39
|
+
|
|
40
|
+
Version 0.2.3 accepts `lesion_mask_path` instead of `bbox_size_mm` to inpaint a supplied lesion shape. Inputs must support the documented crop sizes. Outputs use a RAS 1 mm grid; voxels outside the lesion can change. Synthetic output is not ground truth.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
Apache-2.0; license and third-party notices are included in the distribution. Model weights retain their publisher terms.
|
|
45
|
+
|
|
46
|
+
For research and education. Outputs require human review.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lesiondiffusion-mlx"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "Native MLX implementation of LesionDiffusion"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.15"
|
|
@@ -13,7 +13,7 @@ license = "Apache-2.0 AND MIT AND BSD-3-Clause"
|
|
|
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.5",
|
|
17
17
|
"nibabel>=5.3,<6",
|
|
18
18
|
"numpy>=2.2,<3",
|
|
19
19
|
"safetensors>=0.6,<1",
|
|
@@ -21,11 +21,13 @@ dependencies = [
|
|
|
21
21
|
"tokenizers>=0.21,<1",
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://huggingface.co/MedMLX/lesiondiffusion-mlx"
|
|
26
|
+
|
|
24
27
|
[project.entry-points."medmlx.models"]
|
|
25
28
|
lesiondiffusion = "lesiondiffusion_mlx.medmlx:MODEL"
|
|
26
29
|
|
|
27
30
|
[project.optional-dependencies]
|
|
28
|
-
conversion = ["torch>=2.13,<3"]
|
|
29
31
|
dev = [
|
|
30
32
|
"pyright==1.1.414",
|
|
31
33
|
"pytest>=8.4,<10",
|
|
@@ -41,9 +43,6 @@ reference = ["einops==0.8.2", "omegaconf==2.3.1", "transformers==5.19.0"]
|
|
|
41
43
|
[tool.lesiondiffusion]
|
|
42
44
|
reference = "src/lesiondiffusion_mlx/reference.json"
|
|
43
45
|
|
|
44
|
-
[tool.hatch.metadata]
|
|
45
|
-
allow-direct-references = true
|
|
46
|
-
|
|
47
46
|
[tool.hatch.build.targets.wheel]
|
|
48
47
|
packages = ["src/lesiondiffusion_mlx"]
|
|
49
48
|
|
|
@@ -78,9 +77,6 @@ ban-relative-imports = "all"
|
|
|
78
77
|
"typing.Any" = {msg = "Use a precise type and parse untyped values at the boundary."}
|
|
79
78
|
"typing_extensions.Any" = {msg = "Use a precise type and parse untyped values at the boundary."}
|
|
80
79
|
|
|
81
|
-
[tool.uv.sources]
|
|
82
|
-
medmlx-core = { git = "https://github.com/MedMLX/medmlx.git", rev = "f98e0924f50228e2b9509474947b9672054b3217", subdirectory = "packages/core" }
|
|
83
|
-
|
|
84
80
|
[tool.hatch.build.targets.sdist]
|
|
85
81
|
only-include = [
|
|
86
82
|
"src/lesiondiffusion_mlx",
|
|
@@ -31,9 +31,8 @@ if TYPE_CHECKING:
|
|
|
31
31
|
VqAutoencoder as VqAutoencoder,
|
|
32
32
|
)
|
|
33
33
|
from lesiondiffusion_mlx.mlx_sampling import DdimSampler as DdimSampler
|
|
34
|
-
from lesiondiffusion_mlx.mlx_weights import write_conversion as write_conversion
|
|
35
34
|
|
|
36
|
-
__version__ = "0.2.
|
|
35
|
+
__version__ = "0.2.4"
|
|
37
36
|
|
|
38
37
|
__all__ = [
|
|
39
38
|
"LINET_CONFIG",
|
|
@@ -45,18 +44,16 @@ __all__ = [
|
|
|
45
44
|
"UnetConfig",
|
|
46
45
|
"VqAutoencoder",
|
|
47
46
|
"lesion_diffusion_timesteps",
|
|
48
|
-
"write_conversion",
|
|
49
47
|
]
|
|
50
48
|
|
|
51
49
|
|
|
52
50
|
def __getattr__(name: str) -> object:
|
|
51
|
+
"""Load a public export from its module on first access."""
|
|
53
52
|
if name not in __all__:
|
|
54
53
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
55
54
|
module = (
|
|
56
55
|
"_schedules"
|
|
57
56
|
if name == "lesion_diffusion_timesteps"
|
|
58
|
-
else "mlx_weights"
|
|
59
|
-
if name == "write_conversion"
|
|
60
57
|
else "mlx_sampling"
|
|
61
58
|
if name == "DdimSampler"
|
|
62
59
|
else "mlx_model"
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""FP32 flash attention for 32-wide heads, kept in the projection layout.
|
|
2
|
+
|
|
3
|
+
MLX 0.32 has no fused full-attention kernel for 32-wide FP32 heads. Using its
|
|
4
|
+
64-wide kernel requires zero-padding heads to 64, doing twice the arithmetic.
|
|
5
|
+
With TF32 disabled (medmlx-core's default, which keeps LMNet in full FP32) that
|
|
6
|
+
padded kernel dominates LMNet's step time. This kernel computes the same
|
|
7
|
+
``softmax(Q K^T * scale) V`` in FP32 without padding:
|
|
8
|
+
|
|
9
|
+
* Each simdgroup owns 8 query rows; 32-key blocks of K and V are staged in
|
|
10
|
+
threadgroup memory and shared by the threadgroup's simdgroups.
|
|
11
|
+
* ``Q K^T`` and ``P V`` use FP32 ``simdgroup_float8x8`` multiply-accumulate.
|
|
12
|
+
* The online softmax keeps a running row maximum and sum in FP32, using
|
|
13
|
+
``exp2`` of log2(e)-scaled scores (folded into Q once) with precise exp2.
|
|
14
|
+
Running output rows are rescaled by a diagonal 8x8 matrix multiply.
|
|
15
|
+
|
|
16
|
+
Results agree with MLX's padded FP32 kernel to FP32 rounding; summation order
|
|
17
|
+
differs, so outputs are not bitwise identical.
|
|
18
|
+
|
|
19
|
+
On M5-class GPUs (``nax_available``) a second kernel runs both products on the
|
|
20
|
+
neural accelerators in the exact split-FP16 form of ``_nax_conv``: Q (scaled),
|
|
21
|
+
K, V and each softmax probability ``p`` are split into an FP16 ``hi`` and a
|
|
22
|
+
``2^11``-scaled FP16 ``lo``; ``hi x hi`` and ``hi x lo + lo x hi`` go to
|
|
23
|
+
separate FP32 accumulators. Only the ``lo x lo`` term is dropped, so errors
|
|
24
|
+
stay at FP32 level (measured below the SIMD kernel's), at three FP16 products
|
|
25
|
+
per FP32 product. Each simdgroup owns 16 query rows; a ``16 x 32`` score tile
|
|
26
|
+
already has the register layout of ``P V``'s left operand, so probabilities
|
|
27
|
+
are split in registers. It runs at about the accelerators' FP16 throughput,
|
|
28
|
+
roughly 1.5x the SIMD kernel on LMNet's 23104-token layers.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from functools import lru_cache
|
|
35
|
+
from typing import TYPE_CHECKING, cast
|
|
36
|
+
|
|
37
|
+
from lesiondiffusion_mlx._nax_conv import NAX_MMA_HEADER, nax_available
|
|
38
|
+
from lesiondiffusion_mlx._typing import ArrayConstructor
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from lesiondiffusion_mlx._typing import Array, MetalKernel, Mlx
|
|
42
|
+
|
|
43
|
+
_SIMDGROUPS = 4
|
|
44
|
+
_BLOCK = 32 # query rows per threadgroup and keys per staged block
|
|
45
|
+
_NAX_ROWS = 16 # query rows per simdgroup in the NAX kernel
|
|
46
|
+
|
|
47
|
+
_SOURCE = r"""
|
|
48
|
+
const uint lane = thread_index_in_simdgroup;
|
|
49
|
+
const uint sg = simdgroup_index_in_threadgroup;
|
|
50
|
+
const uint tid = sg * 32 + lane;
|
|
51
|
+
const uint b = threadgroup_position_in_grid.y / H, h = threadgroup_position_in_grid.y % H;
|
|
52
|
+
const uint ld = H * 32;
|
|
53
|
+
const uint row0 = threadgroup_position_in_grid.x * (NSG * 8) + sg * 8;
|
|
54
|
+
const device float* Q = q + ulong(b) * L * ld + h * 32;
|
|
55
|
+
const device float* K = k + ulong(b) * LK * ld + h * 32;
|
|
56
|
+
const device float* V = v + ulong(b) * LK * ld + h * 32;
|
|
57
|
+
|
|
58
|
+
threadgroup float Ks[32 * 32];
|
|
59
|
+
threadgroup float Vs[32 * 32];
|
|
60
|
+
threadgroup float Ss[NSG][8 * 32];
|
|
61
|
+
threadgroup float Dg[NSG][64];
|
|
62
|
+
|
|
63
|
+
for (uint i = lane; i < 64; i += 32) Dg[sg][i] = 0.0f;
|
|
64
|
+
const float sc = scale[0] * M_LOG2E_F;
|
|
65
|
+
simdgroup_float8x8 qf[4];
|
|
66
|
+
simdgroup_float8x8 o[4];
|
|
67
|
+
for (short j = 0; j < 4; j++) {
|
|
68
|
+
simdgroup_load(qf[j], Q + ulong(row0) * ld + j * 8, ld);
|
|
69
|
+
qf[j].thread_elements()[0] *= sc;
|
|
70
|
+
qf[j].thread_elements()[1] *= sc;
|
|
71
|
+
o[j] = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
|
|
72
|
+
}
|
|
73
|
+
// Lane owns row r of the simdgroup's 8 rows and 8 of the block's 32 scores.
|
|
74
|
+
const uint r = lane / 4, c0 = (lane % 4) * 8;
|
|
75
|
+
float m = -INFINITY, l = 0.0f;
|
|
76
|
+
|
|
77
|
+
for (uint kb = 0; kb < LK; kb += 32) {
|
|
78
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
79
|
+
for (uint i = tid; i < 32 * 8; i += NSG * 32) {
|
|
80
|
+
const uint kr = i / 8, kc = (i % 8) * 4;
|
|
81
|
+
*reinterpret_cast<threadgroup float4*>(Ks + kr * 32 + kc) =
|
|
82
|
+
*reinterpret_cast<const device float4*>(K + ulong(kb + kr) * ld + kc);
|
|
83
|
+
*reinterpret_cast<threadgroup float4*>(Vs + kr * 32 + kc) =
|
|
84
|
+
*reinterpret_cast<const device float4*>(V + ulong(kb + kr) * ld + kc);
|
|
85
|
+
}
|
|
86
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
87
|
+
for (short n = 0; n < 4; n++) {
|
|
88
|
+
simdgroup_float8x8 s = make_filled_simdgroup_matrix<float, 8, 8>(0.0f);
|
|
89
|
+
for (short j = 0; j < 4; j++) {
|
|
90
|
+
simdgroup_float8x8 kt;
|
|
91
|
+
simdgroup_load(kt, Ks + n * 8 * 32 + j * 8, 32, ulong2(0, 0), true);
|
|
92
|
+
simdgroup_multiply_accumulate(s, qf[j], kt, s);
|
|
93
|
+
}
|
|
94
|
+
simdgroup_store(s, Ss[sg] + n * 8, 32);
|
|
95
|
+
}
|
|
96
|
+
simdgroup_barrier(mem_flags::mem_threadgroup);
|
|
97
|
+
threadgroup float* row = Ss[sg] + r * 32 + c0;
|
|
98
|
+
float values[8];
|
|
99
|
+
float local = -INFINITY;
|
|
100
|
+
for (short j = 0; j < 8; j++) { values[j] = row[j]; local = max(local, values[j]); }
|
|
101
|
+
local = max(local, simd_shuffle_xor(local, 1));
|
|
102
|
+
local = max(local, simd_shuffle_xor(local, 2));
|
|
103
|
+
const float m_new = max(m, local);
|
|
104
|
+
const float alpha = metal::precise::exp2(m - m_new);
|
|
105
|
+
float total = 0.0f;
|
|
106
|
+
for (short j = 0; j < 8; j++) {
|
|
107
|
+
const float p = metal::precise::exp2(values[j] - m_new);
|
|
108
|
+
total += p;
|
|
109
|
+
row[j] = p;
|
|
110
|
+
}
|
|
111
|
+
total += simd_shuffle_xor(total, 1);
|
|
112
|
+
total += simd_shuffle_xor(total, 2);
|
|
113
|
+
l = l * alpha + total;
|
|
114
|
+
m = m_new;
|
|
115
|
+
if ((lane % 4) == 0) Dg[sg][r * 9] = alpha;
|
|
116
|
+
simdgroup_barrier(mem_flags::mem_threadgroup);
|
|
117
|
+
simdgroup_float8x8 dg;
|
|
118
|
+
simdgroup_load(dg, Dg[sg], 8);
|
|
119
|
+
for (short d = 0; d < 4; d++) { simdgroup_float8x8 t; simdgroup_multiply(t, dg, o[d]); o[d] = t; }
|
|
120
|
+
for (short kk = 0; kk < 4; kk++) {
|
|
121
|
+
simdgroup_float8x8 pf;
|
|
122
|
+
simdgroup_load(pf, Ss[sg] + kk * 8, 32);
|
|
123
|
+
for (short d = 0; d < 4; d++) {
|
|
124
|
+
simdgroup_float8x8 vf;
|
|
125
|
+
simdgroup_load(vf, Vs + kk * 8 * 32 + d * 8, 32);
|
|
126
|
+
simdgroup_multiply_accumulate(o[d], pf, vf, o[d]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
simdgroup_barrier(mem_flags::mem_threadgroup);
|
|
131
|
+
if ((lane % 4) == 0) Dg[sg][r * 9] = 1.0f / l;
|
|
132
|
+
simdgroup_barrier(mem_flags::mem_threadgroup);
|
|
133
|
+
simdgroup_float8x8 dg;
|
|
134
|
+
simdgroup_load(dg, Dg[sg], 8);
|
|
135
|
+
for (short d = 0; d < 4; d++) {
|
|
136
|
+
simdgroup_float8x8 t;
|
|
137
|
+
simdgroup_multiply(t, dg, o[d]);
|
|
138
|
+
simdgroup_store(t, out + ulong(b) * L * ld + ulong(row0) * ld + h * 32 + d * 8, ld);
|
|
139
|
+
}
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_NAX_SOURCE = r"""
|
|
144
|
+
// Fragment element 4 i + j is row fm + 8 i, column fn + j of a 16x16 tile, for
|
|
145
|
+
// left and right operands and the accumulator alike (see mma_16x32x16).
|
|
146
|
+
const uint lane = thread_index_in_simdgroup;
|
|
147
|
+
const uint sg = simdgroup_index_in_threadgroup;
|
|
148
|
+
const uint tid = sg * 32 + lane;
|
|
149
|
+
const uint qid = lane >> 2;
|
|
150
|
+
const uint fm = (qid & 4) | ((lane >> 1) & 3);
|
|
151
|
+
const uint fn = ((qid & 2) | (lane & 1)) * 4;
|
|
152
|
+
const uint b = threadgroup_position_in_grid.y / H, h = threadgroup_position_in_grid.y % H;
|
|
153
|
+
constexpr uint LD = H * 32;
|
|
154
|
+
const uint row0 = threadgroup_position_in_grid.x * (NSG * 16) + sg * 16;
|
|
155
|
+
const ulong qbase = ulong(b) * L * LD + h * 32;
|
|
156
|
+
const ulong kbase = ulong(b) * LK * LD + h * 32;
|
|
157
|
+
|
|
158
|
+
// Q hi/lo for head dims 0-15 (q*0) and 16-31 (q*1); rows past L repeat the
|
|
159
|
+
// last row and are not stored.
|
|
160
|
+
hfrag qh0, qh1, ql0, ql1;
|
|
161
|
+
for (short i = 0; i < 2; i++) {
|
|
162
|
+
const uint r = min(uint(row0 + fm + 8 * i), uint(L - 1));
|
|
163
|
+
for (short j = 0; j < 4; j++) {
|
|
164
|
+
const ulong at = qbase + ulong(r) * LD + fn + j;
|
|
165
|
+
qh0[4 * i + j] = qh[at]; qh1[4 * i + j] = qh[at + 16];
|
|
166
|
+
ql0[4 * i + j] = ql[at]; ql1[4 * i + j] = ql[at + 16];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// 32-key blocks: K transposed (head dim major) and V key major, padded rows.
|
|
170
|
+
constexpr uint KP = 40;
|
|
171
|
+
threadgroup half Kh[32 * KP], Kl[32 * KP], Vh[32 * KP], Vl[32 * KP];
|
|
172
|
+
// o: hi x hi output sums; p: 2^11-scaled cross terms.
|
|
173
|
+
facc o0 = facc(0), o1 = facc(0), p0 = facc(0), p1 = facc(0);
|
|
174
|
+
float m[2] = {-INFINITY, -INFINITY}, l[2] = {0.0f, 0.0f};
|
|
175
|
+
|
|
176
|
+
for (uint kb = 0; kb < LK; kb += 32) {
|
|
177
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
178
|
+
for (uint e = tid; e < 32 * 8; e += NSG * 32) {
|
|
179
|
+
const uint kr = e / 8, kc = (e % 8) * 4;
|
|
180
|
+
const ulong at = kbase + ulong(kb + kr) * LD + kc;
|
|
181
|
+
const half4 k_h = *reinterpret_cast<const device half4*>(kh + at);
|
|
182
|
+
const half4 k_l = *reinterpret_cast<const device half4*>(kl + at);
|
|
183
|
+
for (short t = 0; t < 4; t++) {
|
|
184
|
+
Kh[(kc + t) * KP + kr] = k_h[t];
|
|
185
|
+
Kl[(kc + t) * KP + kr] = k_l[t];
|
|
186
|
+
}
|
|
187
|
+
*reinterpret_cast<threadgroup half4*>(Vh + kr * KP + kc) =
|
|
188
|
+
*reinterpret_cast<const device half4*>(vh + at);
|
|
189
|
+
*reinterpret_cast<threadgroup half4*>(Vl + kr * KP + kc) =
|
|
190
|
+
*reinterpret_cast<const device half4*>(vl + at);
|
|
191
|
+
}
|
|
192
|
+
threadgroup_barrier(mem_flags::mem_threadgroup);
|
|
193
|
+
// Scores of keys kb + 0-15 (s0) and kb + 16-31 (s1), log2(e)-scaled via Q.
|
|
194
|
+
facc s0 = facc(0), s1 = facc(0), t0 = facc(0), t1 = facc(0);
|
|
195
|
+
for (short dc = 0; dc < 2; dc++) {
|
|
196
|
+
hfrag bh0, bh1, bl0, bl1;
|
|
197
|
+
for (short i = 0; i < 2; i++) {
|
|
198
|
+
const uint d = dc * 16 + fm + 8 * i;
|
|
199
|
+
const half4 x0 = *reinterpret_cast<threadgroup half4*>(Kh + d * KP + fn);
|
|
200
|
+
const half4 x1 = *reinterpret_cast<threadgroup half4*>(Kh + d * KP + 16 + fn);
|
|
201
|
+
const half4 y0 = *reinterpret_cast<threadgroup half4*>(Kl + d * KP + fn);
|
|
202
|
+
const half4 y1 = *reinterpret_cast<threadgroup half4*>(Kl + d * KP + 16 + fn);
|
|
203
|
+
for (short j = 0; j < 4; j++) {
|
|
204
|
+
bh0[4 * i + j] = x0[j]; bh1[4 * i + j] = x1[j];
|
|
205
|
+
bl0[4 * i + j] = y0[j]; bl1[4 * i + j] = y1[j];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const thread hfrag& a_h = dc == 0 ? qh0 : qh1;
|
|
209
|
+
const thread hfrag& a_l = dc == 0 ? ql0 : ql1;
|
|
210
|
+
mma_16x32x16(s0, s1, a_h, bh0, bh1);
|
|
211
|
+
mma_16x32x16(t0, t1, a_h, bl0, bl1);
|
|
212
|
+
mma_16x32x16(t0, t1, a_l, bh0, bh1);
|
|
213
|
+
}
|
|
214
|
+
s0 += t0 * (1.0f / 2048.0f);
|
|
215
|
+
s1 += t1 * (1.0f / 2048.0f);
|
|
216
|
+
// Online softmax per row fm + 8 i; a row's 32 scores span lanes xor 1 and 8.
|
|
217
|
+
hfrag ph0, ph1, pl0, pl1;
|
|
218
|
+
for (short i = 0; i < 2; i++) {
|
|
219
|
+
float top = -INFINITY;
|
|
220
|
+
for (short j = 0; j < 4; j++) top = max(top, max(s0[4 * i + j], s1[4 * i + j]));
|
|
221
|
+
top = max(top, simd_shuffle_xor(top, 1));
|
|
222
|
+
top = max(top, simd_shuffle_xor(top, 8));
|
|
223
|
+
const float m_new = max(m[i], top);
|
|
224
|
+
const float alpha = metal::precise::exp2(m[i] - m_new);
|
|
225
|
+
float total = 0.0f;
|
|
226
|
+
for (short j = 0; j < 4; j++) {
|
|
227
|
+
const float e0 = metal::precise::exp2(s0[4 * i + j] - m_new);
|
|
228
|
+
const float e1 = metal::precise::exp2(s1[4 * i + j] - m_new);
|
|
229
|
+
total += e0 + e1;
|
|
230
|
+
const half h0 = half(e0), h1 = half(e1);
|
|
231
|
+
ph0[4 * i + j] = h0; ph1[4 * i + j] = h1;
|
|
232
|
+
pl0[4 * i + j] = half((e0 - float(h0)) * 2048.0f);
|
|
233
|
+
pl1[4 * i + j] = half((e1 - float(h1)) * 2048.0f);
|
|
234
|
+
}
|
|
235
|
+
total += simd_shuffle_xor(total, 1);
|
|
236
|
+
total += simd_shuffle_xor(total, 8);
|
|
237
|
+
l[i] = l[i] * alpha + total;
|
|
238
|
+
m[i] = m_new;
|
|
239
|
+
for (short j = 0; j < 4; j++) {
|
|
240
|
+
o0[4 * i + j] *= alpha; o1[4 * i + j] *= alpha;
|
|
241
|
+
p0[4 * i + j] *= alpha; p1[4 * i + j] *= alpha;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// O += P V over keys kb + 16 kc + 0-15.
|
|
245
|
+
for (short kc = 0; kc < 2; kc++) {
|
|
246
|
+
hfrag vh0, vh1, vl0, vl1;
|
|
247
|
+
for (short i = 0; i < 2; i++) {
|
|
248
|
+
const uint kr = kc * 16 + fm + 8 * i;
|
|
249
|
+
const half4 x0 = *reinterpret_cast<threadgroup half4*>(Vh + kr * KP + fn);
|
|
250
|
+
const half4 x1 = *reinterpret_cast<threadgroup half4*>(Vh + kr * KP + 16 + fn);
|
|
251
|
+
const half4 y0 = *reinterpret_cast<threadgroup half4*>(Vl + kr * KP + fn);
|
|
252
|
+
const half4 y1 = *reinterpret_cast<threadgroup half4*>(Vl + kr * KP + 16 + fn);
|
|
253
|
+
for (short j = 0; j < 4; j++) {
|
|
254
|
+
vh0[4 * i + j] = x0[j]; vh1[4 * i + j] = x1[j];
|
|
255
|
+
vl0[4 * i + j] = y0[j]; vl1[4 * i + j] = y1[j];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const thread hfrag& a_h = kc == 0 ? ph0 : ph1;
|
|
259
|
+
const thread hfrag& a_l = kc == 0 ? pl0 : pl1;
|
|
260
|
+
mma_16x32x16(o0, o1, a_h, vh0, vh1);
|
|
261
|
+
mma_16x32x16(p0, p1, a_h, vl0, vl1);
|
|
262
|
+
mma_16x32x16(p0, p1, a_l, vh0, vh1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (short i = 0; i < 2; i++) {
|
|
266
|
+
const uint r = row0 + fm + 8 * i;
|
|
267
|
+
if (r >= L) continue;
|
|
268
|
+
const float inv = 1.0f / l[i];
|
|
269
|
+
for (short j = 0; j < 4; j++) {
|
|
270
|
+
const ulong at = qbase + ulong(r) * LD + fn + j;
|
|
271
|
+
out[at] = (o0[4 * i + j] + p0[4 * i + j] * (1.0f / 2048.0f)) * inv;
|
|
272
|
+
out[at + 16] = (o1[4 * i + j] + p1[4 * i + j] * (1.0f / 2048.0f)) * inv;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def flash_eligible(q: Array, k: Array, heads: int, mx: Mlx) -> bool:
|
|
279
|
+
"""Whether projections ``(B, L, heads * 32)`` fit the kernel.
|
|
280
|
+
|
|
281
|
+
Requires FP32, 32-wide heads, and query and key lengths divisible by 32.
|
|
282
|
+
"""
|
|
283
|
+
return (
|
|
284
|
+
q.dtype == mx.float32
|
|
285
|
+
and k.dtype == mx.float32
|
|
286
|
+
and int(q.shape[-1]) == heads * 32
|
|
287
|
+
and int(k.shape[-1]) == heads * 32
|
|
288
|
+
and int(q.shape[1]) % _BLOCK == 0
|
|
289
|
+
and int(k.shape[1]) % _BLOCK == 0
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@lru_cache(maxsize=1)
|
|
294
|
+
def _kernel(mx: Mlx) -> MetalKernel:
|
|
295
|
+
"""Build the FP32 SIMD kernel once; used when the NAX kernel is unavailable."""
|
|
296
|
+
return mx.fast.metal_kernel(
|
|
297
|
+
name="lesion_flash_attention_fp32_d32",
|
|
298
|
+
input_names=["q", "k", "v", "scale"],
|
|
299
|
+
output_names=["out"],
|
|
300
|
+
source=_SOURCE,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@lru_cache(maxsize=1)
|
|
305
|
+
def _nax_kernel(mx: Mlx) -> MetalKernel:
|
|
306
|
+
"""Build the NAX kernel once; it takes hi/lo FP16 splits of q, k and v (see ``_split``)."""
|
|
307
|
+
return mx.fast.metal_kernel(
|
|
308
|
+
name="lesion_flash_attention_nax_split_d32",
|
|
309
|
+
input_names=["qh", "ql", "kh", "kl", "vh", "vl"],
|
|
310
|
+
output_names=["out"],
|
|
311
|
+
source=_NAX_SOURCE,
|
|
312
|
+
header=NAX_MMA_HEADER,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _split(x: Array, mx: Mlx) -> tuple[Array, Array]:
|
|
317
|
+
"""FP32 ``x`` as FP16 ``hi = half(x)`` and ``lo = half((x - hi) * 2^11)``."""
|
|
318
|
+
hi = x.astype(mx.float16)
|
|
319
|
+
return hi, ((x - hi.astype(mx.float32)) * 2048.0).astype(mx.float16)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def flash_attention(q: Array, k: Array, v: Array, *, heads: int, scale: float, mx: Mlx) -> Array:
|
|
323
|
+
"""Multi-head attention on head-interleaved projections.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
q: Queries ``(B, L, heads * 32)``; head ``h`` is columns ``32 h`` to ``32 h + 31``.
|
|
327
|
+
k: Keys ``(B, LK, heads * 32)``.
|
|
328
|
+
v: Values ``(B, LK, heads * 32)``.
|
|
329
|
+
heads: Number of heads.
|
|
330
|
+
scale: Multiplier applied to ``Q K^T`` before the softmax.
|
|
331
|
+
mx: The imported ``mlx.core`` module.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
``(B, L, heads * 32)`` FP32 attention output in the same head layout,
|
|
335
|
+
ready for the output projection. On M5-class GPUs the NAX split-FP16
|
|
336
|
+
kernel computes it, which needs ``|q * scale * log2(e)|``, ``|k|`` and
|
|
337
|
+
``|v|`` below 65504; these ranges are not checked. When ``nax_available``
|
|
338
|
+
is false, the FP32 SIMD kernel computes it.
|
|
339
|
+
"""
|
|
340
|
+
batch, length = int(q.shape[0]), int(q.shape[1])
|
|
341
|
+
if nax_available(mx):
|
|
342
|
+
(qh, ql), (kh, kl), (vh, vl) = (
|
|
343
|
+
_split(q * (scale * math.log2(math.e)), mx),
|
|
344
|
+
_split(k, mx),
|
|
345
|
+
_split(v, mx),
|
|
346
|
+
)
|
|
347
|
+
groups = -(-length // (_NAX_ROWS * _SIMDGROUPS))
|
|
348
|
+
return _nax_kernel(mx)(
|
|
349
|
+
inputs=[qh, ql, kh, kl, vh, vl],
|
|
350
|
+
template=[("L", length), ("LK", int(k.shape[1])), ("H", heads), ("NSG", _SIMDGROUPS)],
|
|
351
|
+
grid=(32 * _SIMDGROUPS * groups, batch * heads, 1),
|
|
352
|
+
threadgroup=(32 * _SIMDGROUPS, 1, 1),
|
|
353
|
+
output_shapes=[tuple(q.shape)],
|
|
354
|
+
output_dtypes=[mx.float32],
|
|
355
|
+
)[0]
|
|
356
|
+
return _kernel(mx)(
|
|
357
|
+
inputs=[q, k, v, cast(ArrayConstructor, mx.array)([scale], dtype=mx.float32)],
|
|
358
|
+
template=[("L", length), ("LK", int(k.shape[1])), ("H", heads), ("NSG", _SIMDGROUPS)],
|
|
359
|
+
grid=(32 * _SIMDGROUPS * (length // _BLOCK), batch * heads, 1),
|
|
360
|
+
threadgroup=(32 * _SIMDGROUPS, 1, 1),
|
|
361
|
+
output_shapes=[tuple(q.shape)],
|
|
362
|
+
output_dtypes=[mx.float32],
|
|
363
|
+
)[0]
|