docling-mlx 0.1.0__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.
- docling_mlx/__init__.py +7 -0
- docling_mlx/_compat/__init__.py +3 -0
- docling_mlx/_compat/docling.py +24 -0
- docling_mlx/_compat/mlx_vlm.py +82 -0
- docling_mlx/_models/__init__.py +3 -0
- docling_mlx/_models/detector_primitives.py +318 -0
- docling_mlx/_models/dfine/config.py +467 -0
- docling_mlx/_models/dfine/decoder.py +322 -0
- docling_mlx/_models/dfine/model.py +187 -0
- docling_mlx/_models/dfine/primitives.py +50 -0
- docling_mlx/_models/dfine/vision.py +614 -0
- docling_mlx/_models/dfine/weights.py +165 -0
- docling_mlx/_models/efficientnet/__init__.py +3 -0
- docling_mlx/_models/efficientnet/config.py +263 -0
- docling_mlx/_models/efficientnet/model.py +262 -0
- docling_mlx/_models/efficientnet/weights.py +111 -0
- docling_mlx/_models/rt_detr_v2/__init__.py +21 -0
- docling_mlx/_models/rt_detr_v2/config.py +395 -0
- docling_mlx/_models/rt_detr_v2/model.py +165 -0
- docling_mlx/_models/rt_detr_v2/transformer.py +292 -0
- docling_mlx/_models/rt_detr_v2/vision.py +795 -0
- docling_mlx/_models/rt_detr_v2/weights.py +121 -0
- docling_mlx/_models/tableformer_v1/__init__.py +43 -0
- docling_mlx/_models/tableformer_v1/_source.py +10 -0
- docling_mlx/_models/tableformer_v1/bbox.py +249 -0
- docling_mlx/_models/tableformer_v1/config.py +245 -0
- docling_mlx/_models/tableformer_v1/decoder.py +252 -0
- docling_mlx/_models/tableformer_v1/model.py +102 -0
- docling_mlx/_models/tableformer_v1/vision.py +235 -0
- docling_mlx/_models/tableformer_v2/__init__.py +34 -0
- docling_mlx/_models/tableformer_v2/bbox.py +233 -0
- docling_mlx/_models/tableformer_v2/config.py +101 -0
- docling_mlx/_models/tableformer_v2/decoder.py +458 -0
- docling_mlx/_models/tableformer_v2/model.py +352 -0
- docling_mlx/_models/tableformer_v2/vision.py +292 -0
- docling_mlx/engines/__init__.py +3 -0
- docling_mlx/engines/_shared.py +98 -0
- docling_mlx/engines/image_classification/__init__.py +3 -0
- docling_mlx/engines/image_classification/efficientnet/__init__.py +25 -0
- docling_mlx/engines/image_classification/efficientnet/artifact.py +30 -0
- docling_mlx/engines/image_classification/efficientnet/engine.py +204 -0
- docling_mlx/engines/image_classification/efficientnet/preprocessing.py +160 -0
- docling_mlx/engines/object_detection/__init__.py +3 -0
- docling_mlx/engines/object_detection/_focal_postprocessing.py +117 -0
- docling_mlx/engines/object_detection/_types.py +20 -0
- docling_mlx/engines/object_detection/dfine/__init__.py +25 -0
- docling_mlx/engines/object_detection/dfine/artifact.py +34 -0
- docling_mlx/engines/object_detection/dfine/engine.py +172 -0
- docling_mlx/engines/object_detection/rt_detr_v2/__init__.py +23 -0
- docling_mlx/engines/object_detection/rt_detr_v2/artifact.py +36 -0
- docling_mlx/engines/object_detection/rt_detr_v2/engine.py +175 -0
- docling_mlx/engines/object_detection/rt_detr_v2/preprocessing.py +121 -0
- docling_mlx/engines/table_structure/__init__.py +3 -0
- docling_mlx/engines/table_structure/tableformer_v1/__init__.py +17 -0
- docling_mlx/engines/table_structure/tableformer_v1/artifact.py +173 -0
- docling_mlx/engines/table_structure/tableformer_v1/conversion.py +155 -0
- docling_mlx/engines/table_structure/tableformer_v1/engine.py +236 -0
- docling_mlx/engines/table_structure/tableformer_v1/model_spec.py +22 -0
- docling_mlx/engines/table_structure/tableformer_v1/postprocessing.py +513 -0
- docling_mlx/engines/table_structure/tableformer_v1/preprocessing.py +53 -0
- docling_mlx/engines/table_structure/tableformer_v2/__init__.py +17 -0
- docling_mlx/engines/table_structure/tableformer_v2/artifact.py +208 -0
- docling_mlx/engines/table_structure/tableformer_v2/conversion.py +101 -0
- docling_mlx/engines/table_structure/tableformer_v2/engine.py +208 -0
- docling_mlx/engines/table_structure/tableformer_v2/model_spec.py +22 -0
- docling_mlx/engines/table_structure/tableformer_v2/preprocessing.py +44 -0
- docling_mlx/pipeline.py +115 -0
- docling_mlx/plugins.py +37 -0
- docling_mlx/presets.py +84 -0
- docling_mlx/py.typed +0 -0
- docling_mlx/runtime/__init__.py +3 -0
- docling_mlx/runtime/guards.py +24 -0
- docling_mlx/stages/__init__.py +57 -0
- docling_mlx/stages/_chart_granite.py +110 -0
- docling_mlx/stages/_granite_vision.py +85 -0
- docling_mlx/stages/_otsl.py +95 -0
- docling_mlx/stages/chart_extraction.py +226 -0
- docling_mlx/stages/granite_vision_engine.py +94 -0
- docling_mlx/stages/layout.py +270 -0
- docling_mlx/stages/picture_classification.py +250 -0
- docling_mlx/stages/table_structure.py +213 -0
- docling_mlx/stages/table_structure_v1.py +290 -0
- docling_mlx/stages/table_structure_v2.py +351 -0
- docling_mlx-0.1.0.dist-info/METADATA +216 -0
- docling_mlx-0.1.0.dist-info/RECORD +91 -0
- docling_mlx-0.1.0.dist-info/WHEEL +4 -0
- docling_mlx-0.1.0.dist-info/entry_points.txt +2 -0
- docling_mlx-0.1.0.dist-info/licenses/LICENSE +202 -0
- docling_mlx-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +73 -0
- docling_mlx-0.1.0.dist-info/licenses/LICENSES/MIT.txt +18 -0
- docling_mlx-0.1.0.dist-info/licenses/NOTICE +76 -0
docling_mlx/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Isolate upstream Docling private APIs; the supported lower bound lives in pyproject.toml."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from docling.models.inference_engines.vlm._utils import resolve_model_artifacts_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def require_page_backend(page: object, stage_name: str) -> Any:
|
|
13
|
+
"""Return Docling's internal page backend or preserve the stage error contract."""
|
|
14
|
+
|
|
15
|
+
backend = getattr(page, "_backend", None)
|
|
16
|
+
if backend is None:
|
|
17
|
+
raise RuntimeError(f"{stage_name} requires an initialized page backend")
|
|
18
|
+
return backend
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"require_page_backend",
|
|
23
|
+
"resolve_model_artifacts_path",
|
|
24
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Isolate upstream mlx-vlm private APIs; the supported lower bound lives in pyproject.toml."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def apply_granite_vision_chat_template(
|
|
12
|
+
processor: object,
|
|
13
|
+
messages: list[dict[str, object]],
|
|
14
|
+
add_generation_prompt: bool,
|
|
15
|
+
**kwargs: object,
|
|
16
|
+
) -> Any:
|
|
17
|
+
"""Apply mlx-vlm's template without importing it until Granite is loaded."""
|
|
18
|
+
|
|
19
|
+
from mlx_vlm.prompt_utils import get_chat_template
|
|
20
|
+
|
|
21
|
+
return get_chat_template(processor, messages, add_generation_prompt, **cast(Any, kwargs))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_granite_vision_config(artifact_path: Path) -> object:
|
|
25
|
+
"""Load the Granite config through mlx-vlm lazily."""
|
|
26
|
+
|
|
27
|
+
from mlx_vlm.utils import load_config
|
|
28
|
+
|
|
29
|
+
return load_config(artifact_path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_granite_vision_model(artifact_path: Path) -> tuple[object, object]:
|
|
33
|
+
"""Strictly load Granite through mlx-vlm lazily."""
|
|
34
|
+
|
|
35
|
+
from mlx_vlm import load
|
|
36
|
+
|
|
37
|
+
return load(str(artifact_path), strict=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def correct_loaded_granite_vision_activations(model: object, *, gelu_type: type[Any]) -> None:
|
|
41
|
+
"""Apply the Granite Vision activation correction."""
|
|
42
|
+
|
|
43
|
+
vision_model = model.vision_tower.vision_model # type: ignore[attr-defined]
|
|
44
|
+
layers = vision_model.encoder.layers
|
|
45
|
+
if len(layers) != 27:
|
|
46
|
+
raise ValueError("Granite Vision model must contain exactly 27 encoder layers")
|
|
47
|
+
for layer in layers:
|
|
48
|
+
layer.mlp.activation_fn = gelu_type(approx="tanh")
|
|
49
|
+
vision_model.head.mlp.activation_fn = gelu_type(approx="tanh")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def replace_granite_vision_image_processor(
|
|
53
|
+
processor: object,
|
|
54
|
+
artifact_path: Path,
|
|
55
|
+
*,
|
|
56
|
+
auto_image_processor: type[Any],
|
|
57
|
+
processor_type: type[Any] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Require mlx-vlm's processor before installing Docling's torchvision backend."""
|
|
60
|
+
|
|
61
|
+
if processor_type is None:
|
|
62
|
+
from mlx_vlm.models.granite4_vision.processing_granite4_vision import (
|
|
63
|
+
Granite4VisionProcessor,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
processor_type = Granite4VisionProcessor
|
|
67
|
+
if not isinstance(processor, processor_type):
|
|
68
|
+
raise TypeError("unexpected Granite Vision processor type")
|
|
69
|
+
|
|
70
|
+
replacement = auto_image_processor.from_pretrained(artifact_path)
|
|
71
|
+
if getattr(replacement, "backend", None) != "torchvision":
|
|
72
|
+
raise RuntimeError("Granite Vision 4.1 requires the torchvision image processor")
|
|
73
|
+
processor.image_processor = replacement
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"apply_granite_vision_chat_template",
|
|
78
|
+
"correct_loaded_granite_vision_activations",
|
|
79
|
+
"load_granite_vision_config",
|
|
80
|
+
"load_granite_vision_model",
|
|
81
|
+
"replace_granite_vision_image_processor",
|
|
82
|
+
]
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Adapted from mlx-vlm (mlx_vlm/models/rt_detr_v2).
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Exact numerical primitives shared by the native detector decoders.
|
|
4
|
+
|
|
5
|
+
The implementation has the same semantics as ``torch.nn.functional.grid_sample``
|
|
6
|
+
for its only supported contract: channel-last FP32 tensors, bilinear sampling,
|
|
7
|
+
zero padding, and ``align_corners=False``. It is intentionally not a general
|
|
8
|
+
image-resampling API.
|
|
9
|
+
|
|
10
|
+
The readable composed implementation is retained as a differential oracle for
|
|
11
|
+
the Metal kernel. The production kernel checks every neighbour is in bounds
|
|
12
|
+
*before* calculating or dereferencing its source address.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import mlx.core as mx
|
|
22
|
+
import mlx.nn as nn
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def inverse_sigmoid(x: mx.array, eps: float = 1e-5) -> mx.array:
|
|
26
|
+
"""Return the stable logit transform used for detector reference boxes."""
|
|
27
|
+
if not isinstance(x, mx.array):
|
|
28
|
+
raise TypeError("x must be an MLX array")
|
|
29
|
+
if not 0.0 < eps < 0.5:
|
|
30
|
+
raise ValueError("eps must be greater than zero and less than 0.5")
|
|
31
|
+
clipped = mx.clip(x, 0.0, 1.0)
|
|
32
|
+
numerator = mx.clip(clipped, eps, 1.0)
|
|
33
|
+
denominator = mx.clip(1.0 - clipped, eps, 1.0)
|
|
34
|
+
return mx.log(numerator / denominator)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_spatial_shapes(
|
|
38
|
+
spatial_shapes: Sequence[tuple[int, int]],
|
|
39
|
+
) -> tuple[tuple[int, int], ...]:
|
|
40
|
+
return tuple((int(height), int(width)) for height, width in spatial_shapes)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_anchors(
|
|
44
|
+
spatial_shapes: Sequence[tuple[int, int]],
|
|
45
|
+
grid_size: float = 0.05,
|
|
46
|
+
dtype: mx.Dtype = mx.float32,
|
|
47
|
+
) -> tuple[mx.array, mx.array]:
|
|
48
|
+
"""Generate encoder-query anchor logits and their valid mask."""
|
|
49
|
+
shapes = _validate_spatial_shapes(spatial_shapes)
|
|
50
|
+
|
|
51
|
+
anchors_per_level = []
|
|
52
|
+
for level, (height, width) in enumerate(shapes):
|
|
53
|
+
grid_y, grid_x = mx.meshgrid(
|
|
54
|
+
mx.arange(height, dtype=dtype), mx.arange(width, dtype=dtype), indexing="ij"
|
|
55
|
+
)
|
|
56
|
+
grid_xy = mx.stack([grid_x, grid_y], axis=-1)[None, ...] + 0.5
|
|
57
|
+
grid_xy = grid_xy / mx.array([width, height], dtype=dtype)[None, None, None, :]
|
|
58
|
+
box_size = mx.ones_like(grid_xy) * grid_size * (2.0**level)
|
|
59
|
+
anchors_per_level.append(
|
|
60
|
+
mx.concatenate([grid_xy, box_size], axis=-1).reshape(1, height * width, 4)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
anchors = mx.concatenate(anchors_per_level, axis=1)
|
|
64
|
+
eps = 1e-2
|
|
65
|
+
valid_mask = ((anchors > eps) & (anchors < 1.0 - eps)).all(axis=-1, keepdims=True)
|
|
66
|
+
anchor_logits = mx.log(anchors / (1.0 - anchors))
|
|
67
|
+
invalid_logit = mx.array(mx.finfo(dtype).max, dtype=dtype)
|
|
68
|
+
return mx.where(valid_mask, anchor_logits, invalid_logit), valid_mask
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def select_encoder_queries(
|
|
72
|
+
memory: mx.array,
|
|
73
|
+
scores: mx.array,
|
|
74
|
+
box_logits: mx.array,
|
|
75
|
+
num_queries: int,
|
|
76
|
+
) -> tuple[mx.array, mx.array, mx.array, mx.array]:
|
|
77
|
+
"""Select detector proposals and gather their decoder inputs."""
|
|
78
|
+
scores_max = scores.max(axis=-1)
|
|
79
|
+
indices = mx.argpartition(-scores_max, num_queries - 1, axis=1)[:, :num_queries]
|
|
80
|
+
selected_scores = mx.take_along_axis(scores_max, indices, axis=1)
|
|
81
|
+
indices = mx.take_along_axis(indices, mx.argsort(-selected_scores, axis=1), axis=1)
|
|
82
|
+
|
|
83
|
+
def gather(values: mx.array) -> mx.array:
|
|
84
|
+
expanded = mx.broadcast_to(indices[:, :, None], (*indices.shape, values.shape[-1]))
|
|
85
|
+
return mx.take_along_axis(values, expanded, axis=1)
|
|
86
|
+
|
|
87
|
+
reference_logits = gather(box_logits)
|
|
88
|
+
return (
|
|
89
|
+
gather(scores),
|
|
90
|
+
mx.sigmoid(reference_logits),
|
|
91
|
+
mx.stop_gradient(reference_logits),
|
|
92
|
+
mx.stop_gradient(gather(memory)),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MLP(nn.Module):
|
|
97
|
+
"""Linear stack with ReLU between layers and checkpoint-compatible keys."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, input_dim: int, hidden_dim: int, output_dim: int, num_layers: int) -> None:
|
|
100
|
+
super().__init__()
|
|
101
|
+
dims = [input_dim, *([hidden_dim] * (num_layers - 1)), output_dim]
|
|
102
|
+
self.layers = [nn.Linear(dims[index], dims[index + 1]) for index in range(num_layers)]
|
|
103
|
+
|
|
104
|
+
def __call__(self, x: mx.array) -> mx.array:
|
|
105
|
+
for index, layer in enumerate(self.layers):
|
|
106
|
+
x = layer(x)
|
|
107
|
+
if index < len(self.layers) - 1:
|
|
108
|
+
x = nn.relu(x)
|
|
109
|
+
return x
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SelfAttention(nn.Module):
|
|
113
|
+
"""Positional q/k self-attention with shared detector state keys."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
hidden_dim: int,
|
|
118
|
+
num_heads: int,
|
|
119
|
+
*,
|
|
120
|
+
use_fast_attention: bool = False,
|
|
121
|
+
) -> None:
|
|
122
|
+
super().__init__()
|
|
123
|
+
self._use_fast_attention = use_fast_attention
|
|
124
|
+
self.num_heads = num_heads
|
|
125
|
+
self.head_dim = hidden_dim // num_heads
|
|
126
|
+
self.scale = self.head_dim**-0.5
|
|
127
|
+
self.q_proj = nn.Linear(hidden_dim, hidden_dim)
|
|
128
|
+
self.k_proj = nn.Linear(hidden_dim, hidden_dim)
|
|
129
|
+
self.v_proj = nn.Linear(hidden_dim, hidden_dim)
|
|
130
|
+
self.out_proj = nn.Linear(hidden_dim, hidden_dim)
|
|
131
|
+
|
|
132
|
+
def __call__(
|
|
133
|
+
self,
|
|
134
|
+
hidden_states: mx.array,
|
|
135
|
+
position_embeddings: mx.array | None = None,
|
|
136
|
+
) -> mx.array:
|
|
137
|
+
batch, length, hidden_dim = hidden_states.shape
|
|
138
|
+
query_key_input = (
|
|
139
|
+
hidden_states + position_embeddings
|
|
140
|
+
if position_embeddings is not None
|
|
141
|
+
else hidden_states
|
|
142
|
+
)
|
|
143
|
+
query = (
|
|
144
|
+
self.q_proj(query_key_input)
|
|
145
|
+
.reshape(batch, length, self.num_heads, self.head_dim)
|
|
146
|
+
.transpose(0, 2, 1, 3)
|
|
147
|
+
)
|
|
148
|
+
key = (
|
|
149
|
+
self.k_proj(query_key_input)
|
|
150
|
+
.reshape(batch, length, self.num_heads, self.head_dim)
|
|
151
|
+
.transpose(0, 2, 1, 3)
|
|
152
|
+
)
|
|
153
|
+
value = (
|
|
154
|
+
self.v_proj(hidden_states)
|
|
155
|
+
.reshape(batch, length, self.num_heads, self.head_dim)
|
|
156
|
+
.transpose(0, 2, 1, 3)
|
|
157
|
+
)
|
|
158
|
+
if self._use_fast_attention:
|
|
159
|
+
output = mx.fast.scaled_dot_product_attention(query, key, value, scale=self.scale)
|
|
160
|
+
else:
|
|
161
|
+
attention = mx.softmax(
|
|
162
|
+
(query @ key.transpose(0, 1, 3, 2)) * self.scale,
|
|
163
|
+
axis=-1,
|
|
164
|
+
)
|
|
165
|
+
output = attention @ value
|
|
166
|
+
output = output.transpose(0, 2, 1, 3).reshape(batch, length, hidden_dim)
|
|
167
|
+
return self.out_proj(output)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
_METAL_SOURCE = """
|
|
171
|
+
const size_t element = thread_position_in_grid.x;
|
|
172
|
+
const size_t batch_size = size_t(grid_shape[0]);
|
|
173
|
+
const size_t output_height = size_t(grid_shape[1]);
|
|
174
|
+
const size_t output_width = size_t(grid_shape[2]);
|
|
175
|
+
const size_t channels = size_t(x_shape[3]);
|
|
176
|
+
const size_t output_count = batch_size * output_height * output_width * channels;
|
|
177
|
+
if (element >= output_count) return;
|
|
178
|
+
|
|
179
|
+
const int input_height = int(x_shape[1]);
|
|
180
|
+
const int input_width = int(x_shape[2]);
|
|
181
|
+
const size_t channel = element % channels;
|
|
182
|
+
const size_t sample = element / channels;
|
|
183
|
+
const size_t grid_offset = sample * 2;
|
|
184
|
+
const size_t batch = sample / (output_height * output_width);
|
|
185
|
+
|
|
186
|
+
// align_corners=False maps normalized coordinates to pixel centres.
|
|
187
|
+
// Keep the scale and shift as separate FP32 operations. Collapsing them
|
|
188
|
+
// lets Metal reassociate the expression and diverge from Torch/MLX.
|
|
189
|
+
const float x_scaled = (grid[grid_offset] + 1.0f) * float(input_width);
|
|
190
|
+
const float y_scaled = (grid[grid_offset + 1] + 1.0f) * float(input_height);
|
|
191
|
+
const float x_coord = (x_scaled - 1.0f) * 0.5f;
|
|
192
|
+
const float y_coord = (y_scaled - 1.0f) * 0.5f;
|
|
193
|
+
const int x0 = int(floor(x_coord));
|
|
194
|
+
const int y0 = int(floor(y_coord));
|
|
195
|
+
const int x1 = x0 + 1;
|
|
196
|
+
const int y1 = y0 + 1;
|
|
197
|
+
|
|
198
|
+
const T w00 = T((float(x1) - x_coord) * (float(y1) - y_coord));
|
|
199
|
+
const T w01 = T((x_coord - float(x0)) * (float(y1) - y_coord));
|
|
200
|
+
const T w10 = T((float(x1) - x_coord) * (y_coord - float(y0)));
|
|
201
|
+
const T w11 = T((x_coord - float(x0)) * (y_coord - float(y0)));
|
|
202
|
+
|
|
203
|
+
const size_t batch_offset = batch * size_t(input_height) * size_t(input_width) * channels;
|
|
204
|
+
T value00 = T(0);
|
|
205
|
+
T value01 = T(0);
|
|
206
|
+
T value10 = T(0);
|
|
207
|
+
T value11 = T(0);
|
|
208
|
+
|
|
209
|
+
// Do not move these reads into conditional expressions: a source address
|
|
210
|
+
// is formed and dereferenced only after its coordinate has been checked.
|
|
211
|
+
if (y0 >= 0 && y0 < input_height && x0 >= 0 && x0 < input_width) {
|
|
212
|
+
const size_t source = batch_offset
|
|
213
|
+
+ (size_t(y0) * size_t(input_width) + size_t(x0)) * channels + channel;
|
|
214
|
+
value00 = x[source];
|
|
215
|
+
}
|
|
216
|
+
if (y0 >= 0 && y0 < input_height && x1 >= 0 && x1 < input_width) {
|
|
217
|
+
const size_t source = batch_offset
|
|
218
|
+
+ (size_t(y0) * size_t(input_width) + size_t(x1)) * channels + channel;
|
|
219
|
+
value01 = x[source];
|
|
220
|
+
}
|
|
221
|
+
if (y1 >= 0 && y1 < input_height && x0 >= 0 && x0 < input_width) {
|
|
222
|
+
const size_t source = batch_offset
|
|
223
|
+
+ (size_t(y1) * size_t(input_width) + size_t(x0)) * channels + channel;
|
|
224
|
+
value10 = x[source];
|
|
225
|
+
}
|
|
226
|
+
if (y1 >= 0 && y1 < input_height && x1 >= 0 && x1 < input_width) {
|
|
227
|
+
const size_t source = batch_offset
|
|
228
|
+
+ (size_t(y1) * size_t(input_width) + size_t(x1)) * channels + channel;
|
|
229
|
+
value11 = x[source];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
out[element] = w00 * value00 + w01 * value01 + w10 * value10 + w11 * value11;
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _validate_inputs(x: mx.array, grid: mx.array) -> tuple[int, int, int, int, int, int]:
|
|
237
|
+
if not isinstance(x, mx.array) or not isinstance(grid, mx.array):
|
|
238
|
+
raise TypeError("x and grid must be MLX arrays")
|
|
239
|
+
if x.ndim != 4:
|
|
240
|
+
raise ValueError("x must have shape [B, H, W, C]")
|
|
241
|
+
if grid.ndim != 4 or grid.shape[-1] != 2:
|
|
242
|
+
raise ValueError("grid must have shape [B, out_h, out_w, 2]")
|
|
243
|
+
batch, height, width, channels = (int(dim) for dim in x.shape)
|
|
244
|
+
grid_batch, output_height, output_width, _ = (int(dim) for dim in grid.shape)
|
|
245
|
+
if grid_batch != batch:
|
|
246
|
+
raise ValueError("x and grid must have the same batch size")
|
|
247
|
+
if height < 1 or width < 1 or channels < 1:
|
|
248
|
+
raise ValueError("x spatial dimensions and channels must be nonempty")
|
|
249
|
+
if x.dtype != mx.float32 or grid.dtype != mx.float32:
|
|
250
|
+
raise TypeError("detector grid sampling requires FP32 x and grid arrays")
|
|
251
|
+
return batch, height, width, channels, output_height, output_width
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def grid_sample_bilinear_zeros_align_corners_false_reference(
|
|
255
|
+
x: mx.array, grid: mx.array
|
|
256
|
+
) -> mx.array:
|
|
257
|
+
"""Return the composed-MLX reference implementation for the narrow contract."""
|
|
258
|
+
batch, height, width, channels, output_height, output_width = _validate_inputs(x, grid)
|
|
259
|
+
if batch == 0 or output_height == 0 or output_width == 0:
|
|
260
|
+
return mx.zeros((batch, output_height, output_width, channels), dtype=mx.float32)
|
|
261
|
+
|
|
262
|
+
x_coord = ((grid[..., 0] + 1.0) * width - 1.0) * 0.5
|
|
263
|
+
y_coord = ((grid[..., 1] + 1.0) * height - 1.0) * 0.5
|
|
264
|
+
x0 = mx.floor(x_coord).astype(mx.int32)
|
|
265
|
+
y0 = mx.floor(y_coord).astype(mx.int32)
|
|
266
|
+
x1 = x0 + 1
|
|
267
|
+
y1 = y0 + 1
|
|
268
|
+
|
|
269
|
+
w00 = ((x1.astype(mx.float32) - x_coord) * (y1.astype(mx.float32) - y_coord))[..., None]
|
|
270
|
+
w01 = ((x_coord - x0.astype(mx.float32)) * (y1.astype(mx.float32) - y_coord))[..., None]
|
|
271
|
+
w10 = ((x1.astype(mx.float32) - x_coord) * (y_coord - y0.astype(mx.float32)))[..., None]
|
|
272
|
+
w11 = ((x_coord - x0.astype(mx.float32)) * (y_coord - y0.astype(mx.float32)))[..., None]
|
|
273
|
+
flattened = x.reshape(batch, height * width, channels)
|
|
274
|
+
batch_indices = mx.arange(batch)[:, None]
|
|
275
|
+
|
|
276
|
+
def gather(y_indices: mx.array, x_indices: mx.array) -> mx.array:
|
|
277
|
+
valid = (y_indices >= 0) & (y_indices < height) & (x_indices >= 0) & (x_indices < width)
|
|
278
|
+
clipped_y = mx.clip(y_indices, 0, height - 1)
|
|
279
|
+
clipped_x = mx.clip(x_indices, 0, width - 1)
|
|
280
|
+
indices = (clipped_y * width + clipped_x).reshape(batch, -1)
|
|
281
|
+
values = flattened[batch_indices, indices].reshape(
|
|
282
|
+
batch, output_height, output_width, channels
|
|
283
|
+
)
|
|
284
|
+
return values * valid[..., None]
|
|
285
|
+
|
|
286
|
+
return w00 * gather(y0, x0) + w01 * gather(y0, x1) + w10 * gather(y1, x0) + w11 * gather(y1, x1)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@lru_cache(maxsize=1)
|
|
290
|
+
def _grid_sample_kernel() -> Any:
|
|
291
|
+
return mx.fast.metal_kernel(
|
|
292
|
+
name="docling_rt_detr_v2_grid_sample_bilinear_zeros_ac_false",
|
|
293
|
+
input_names=["x", "grid"],
|
|
294
|
+
output_names=["out"],
|
|
295
|
+
source=_METAL_SOURCE,
|
|
296
|
+
compile_options={"math_mode": "safe"},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def grid_sample_bilinear_zeros_align_corners_false(x: mx.array, grid: mx.array) -> mx.array:
|
|
301
|
+
"""Sample ``x[B,H,W,C]`` at normalized ``grid[B,out_h,out_w,2]`` coordinates.
|
|
302
|
+
|
|
303
|
+
Only the detectors' FP32 bilinear/zeros/``align_corners=False`` operation is
|
|
304
|
+
supported. The result has shape ``[B, out_h, out_w, C]``.
|
|
305
|
+
"""
|
|
306
|
+
batch, _, _, channels, output_height, output_width = _validate_inputs(x, grid)
|
|
307
|
+
output_shape = (batch, output_height, output_width, channels)
|
|
308
|
+
if batch == 0 or output_height == 0 or output_width == 0:
|
|
309
|
+
return mx.zeros(output_shape, dtype=mx.float32)
|
|
310
|
+
kernel = _grid_sample_kernel()
|
|
311
|
+
return kernel(
|
|
312
|
+
inputs=[mx.contiguous(x), mx.contiguous(grid)],
|
|
313
|
+
template=[("T", mx.float32)],
|
|
314
|
+
output_shapes=[output_shape],
|
|
315
|
+
output_dtypes=[mx.float32],
|
|
316
|
+
grid=(batch * output_height * output_width * channels, 1, 1),
|
|
317
|
+
threadgroup=(256, 1, 1),
|
|
318
|
+
)[0]
|