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.
Files changed (91) hide show
  1. docling_mlx/__init__.py +7 -0
  2. docling_mlx/_compat/__init__.py +3 -0
  3. docling_mlx/_compat/docling.py +24 -0
  4. docling_mlx/_compat/mlx_vlm.py +82 -0
  5. docling_mlx/_models/__init__.py +3 -0
  6. docling_mlx/_models/detector_primitives.py +318 -0
  7. docling_mlx/_models/dfine/config.py +467 -0
  8. docling_mlx/_models/dfine/decoder.py +322 -0
  9. docling_mlx/_models/dfine/model.py +187 -0
  10. docling_mlx/_models/dfine/primitives.py +50 -0
  11. docling_mlx/_models/dfine/vision.py +614 -0
  12. docling_mlx/_models/dfine/weights.py +165 -0
  13. docling_mlx/_models/efficientnet/__init__.py +3 -0
  14. docling_mlx/_models/efficientnet/config.py +263 -0
  15. docling_mlx/_models/efficientnet/model.py +262 -0
  16. docling_mlx/_models/efficientnet/weights.py +111 -0
  17. docling_mlx/_models/rt_detr_v2/__init__.py +21 -0
  18. docling_mlx/_models/rt_detr_v2/config.py +395 -0
  19. docling_mlx/_models/rt_detr_v2/model.py +165 -0
  20. docling_mlx/_models/rt_detr_v2/transformer.py +292 -0
  21. docling_mlx/_models/rt_detr_v2/vision.py +795 -0
  22. docling_mlx/_models/rt_detr_v2/weights.py +121 -0
  23. docling_mlx/_models/tableformer_v1/__init__.py +43 -0
  24. docling_mlx/_models/tableformer_v1/_source.py +10 -0
  25. docling_mlx/_models/tableformer_v1/bbox.py +249 -0
  26. docling_mlx/_models/tableformer_v1/config.py +245 -0
  27. docling_mlx/_models/tableformer_v1/decoder.py +252 -0
  28. docling_mlx/_models/tableformer_v1/model.py +102 -0
  29. docling_mlx/_models/tableformer_v1/vision.py +235 -0
  30. docling_mlx/_models/tableformer_v2/__init__.py +34 -0
  31. docling_mlx/_models/tableformer_v2/bbox.py +233 -0
  32. docling_mlx/_models/tableformer_v2/config.py +101 -0
  33. docling_mlx/_models/tableformer_v2/decoder.py +458 -0
  34. docling_mlx/_models/tableformer_v2/model.py +352 -0
  35. docling_mlx/_models/tableformer_v2/vision.py +292 -0
  36. docling_mlx/engines/__init__.py +3 -0
  37. docling_mlx/engines/_shared.py +98 -0
  38. docling_mlx/engines/image_classification/__init__.py +3 -0
  39. docling_mlx/engines/image_classification/efficientnet/__init__.py +25 -0
  40. docling_mlx/engines/image_classification/efficientnet/artifact.py +30 -0
  41. docling_mlx/engines/image_classification/efficientnet/engine.py +204 -0
  42. docling_mlx/engines/image_classification/efficientnet/preprocessing.py +160 -0
  43. docling_mlx/engines/object_detection/__init__.py +3 -0
  44. docling_mlx/engines/object_detection/_focal_postprocessing.py +117 -0
  45. docling_mlx/engines/object_detection/_types.py +20 -0
  46. docling_mlx/engines/object_detection/dfine/__init__.py +25 -0
  47. docling_mlx/engines/object_detection/dfine/artifact.py +34 -0
  48. docling_mlx/engines/object_detection/dfine/engine.py +172 -0
  49. docling_mlx/engines/object_detection/rt_detr_v2/__init__.py +23 -0
  50. docling_mlx/engines/object_detection/rt_detr_v2/artifact.py +36 -0
  51. docling_mlx/engines/object_detection/rt_detr_v2/engine.py +175 -0
  52. docling_mlx/engines/object_detection/rt_detr_v2/preprocessing.py +121 -0
  53. docling_mlx/engines/table_structure/__init__.py +3 -0
  54. docling_mlx/engines/table_structure/tableformer_v1/__init__.py +17 -0
  55. docling_mlx/engines/table_structure/tableformer_v1/artifact.py +173 -0
  56. docling_mlx/engines/table_structure/tableformer_v1/conversion.py +155 -0
  57. docling_mlx/engines/table_structure/tableformer_v1/engine.py +236 -0
  58. docling_mlx/engines/table_structure/tableformer_v1/model_spec.py +22 -0
  59. docling_mlx/engines/table_structure/tableformer_v1/postprocessing.py +513 -0
  60. docling_mlx/engines/table_structure/tableformer_v1/preprocessing.py +53 -0
  61. docling_mlx/engines/table_structure/tableformer_v2/__init__.py +17 -0
  62. docling_mlx/engines/table_structure/tableformer_v2/artifact.py +208 -0
  63. docling_mlx/engines/table_structure/tableformer_v2/conversion.py +101 -0
  64. docling_mlx/engines/table_structure/tableformer_v2/engine.py +208 -0
  65. docling_mlx/engines/table_structure/tableformer_v2/model_spec.py +22 -0
  66. docling_mlx/engines/table_structure/tableformer_v2/preprocessing.py +44 -0
  67. docling_mlx/pipeline.py +115 -0
  68. docling_mlx/plugins.py +37 -0
  69. docling_mlx/presets.py +84 -0
  70. docling_mlx/py.typed +0 -0
  71. docling_mlx/runtime/__init__.py +3 -0
  72. docling_mlx/runtime/guards.py +24 -0
  73. docling_mlx/stages/__init__.py +57 -0
  74. docling_mlx/stages/_chart_granite.py +110 -0
  75. docling_mlx/stages/_granite_vision.py +85 -0
  76. docling_mlx/stages/_otsl.py +95 -0
  77. docling_mlx/stages/chart_extraction.py +226 -0
  78. docling_mlx/stages/granite_vision_engine.py +94 -0
  79. docling_mlx/stages/layout.py +270 -0
  80. docling_mlx/stages/picture_classification.py +250 -0
  81. docling_mlx/stages/table_structure.py +213 -0
  82. docling_mlx/stages/table_structure_v1.py +290 -0
  83. docling_mlx/stages/table_structure_v2.py +351 -0
  84. docling_mlx-0.1.0.dist-info/METADATA +216 -0
  85. docling_mlx-0.1.0.dist-info/RECORD +91 -0
  86. docling_mlx-0.1.0.dist-info/WHEEL +4 -0
  87. docling_mlx-0.1.0.dist-info/entry_points.txt +2 -0
  88. docling_mlx-0.1.0.dist-info/licenses/LICENSE +202 -0
  89. docling_mlx-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +73 -0
  90. docling_mlx-0.1.0.dist-info/licenses/LICENSES/MIT.txt +18 -0
  91. docling_mlx-0.1.0.dist-info/licenses/NOTICE +76 -0
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """MLX components for Docling, without import-time model loading or patching."""
4
+
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("docling-mlx")
@@ -0,0 +1,3 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Upstream compatibility boundaries at the supported lower bounds."""
@@ -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,3 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Private native MLX architectures not yet supplied by a compatible upstream."""
@@ -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]