-
Notifications
You must be signed in to change notification settings - Fork 633
Expand file tree
/
Copy patharray_api.py
More file actions
393 lines (330 loc) · 13.9 KB
/
Copy patharray_api.py
File metadata and controls
393 lines (330 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# SPDX-License-Identifier: LGPL-3.0-or-later
"""Utilities for the array API."""
from typing import (
Any,
)
import array_api_compat
import numpy as np
from packaging.version import (
Version,
)
# Type alias for array_api compatible arrays
Array = np.ndarray | Any # Any to support JAX, PyTorch, etc. arrays
def xp_asarray_nodetach(
xp: Any,
obj: Any,
*,
dtype: Any = None,
device: Any = None,
) -> Array:
"""``xp.asarray`` that preserves autograd for backend tensors.
``torch.asarray`` detaches its input from the autograd graph, so calling
``xp.asarray`` on a weight attribute that is already a backend tensor
(e.g. a ``torch.nn.Parameter`` registered by the pt_expt backend)
silently breaks gradient flow to that weight. This helper converts
genuine non-backend data (numpy arrays, python scalars/lists) via
``xp.asarray``; backend tensors are returned as-is, with an optional
differentiable dtype cast via ``xp.astype``.
The ``device`` argument only applies to the conversion path: backend
tensors are assumed to already live on the working device (they are
created together with the inputs).
"""
if isinstance(obj, np.ndarray) or not array_api_compat.is_array_api_obj(obj):
if dtype is None:
return xp.asarray(obj, device=device)
return xp.asarray(obj, dtype=dtype, device=device)
if dtype is not None and obj.dtype != dtype:
obj = xp.astype(obj, dtype)
return obj
# array api adds take_along_axis in https://github.com/data-apis/array-api/pull/816
# but it hasn't been released yet
# below is a pure Python implementation of take_along_axis
# https://github.com/data-apis/array-api/issues/177#issuecomment-2093630595
def xp_swapaxes(a: Array, axis1: int, axis2: int) -> Array:
xp = array_api_compat.array_namespace(a)
axes = list(range(a.ndim))
axes[axis1], axes[axis2] = axes[axis2], axes[axis1]
a = xp.permute_dims(a, axes)
return a
def xp_take_along_axis(arr: Array, indices: Array, axis: int) -> Array:
xp = array_api_compat.array_namespace(arr)
# torch.take_along_dim requires int64 indices
if array_api_compat.is_torch_array(indices):
indices = xp.astype(indices, xp.int64)
if array_api_compat.is_torch_array(arr):
# Use torch.gather directly for torch.export dynamic shape compatibility.
# array_api_compat's take_along_axis / torch.take_along_dim specializes
# the source dimension size to a constant during torch.export tracing,
# breaking dynamic shape export. torch.gather is the underlying
# primitive and handles symbolic shapes correctly.
import torch
return torch.gather(arr, axis, indices)
if Version(xp.__array_api_version__) >= Version("2024.12"):
# see: https://github.com/data-apis/array-api-strict/blob/d086c619a58f35c38240592ef994aa19ca7beebc/array_api_strict/_indexing_functions.py#L30-L39
return xp.take_along_axis(arr, indices, axis=axis)
arr = xp_swapaxes(arr, axis, -1)
indices = xp_swapaxes(indices, axis, -1)
m = arr.shape[-1]
n = indices.shape[-1]
shape = list(arr.shape)
shape.pop(-1)
shape = (*shape, n)
arr = xp.reshape(arr, (-1,))
if n != 0:
indices = xp.reshape(indices, (-1, n))
else:
indices = xp.reshape(indices, (0, 0))
dev = array_api_compat.device(indices)
offset = (xp.arange(indices.shape[0], dtype=indices.dtype, device=dev) * m)[
:, xp.newaxis
]
indices = xp.reshape(offset + indices, (-1,))
out = xp.take(arr, indices)
out = xp.reshape(out, shape)
return xp_swapaxes(out, axis, -1)
def xp_take_first_n(arr: Array, dim: int, n: int) -> Array:
"""Take the first *n* elements along *dim*.
For torch tensors, uses ``torch.index_select`` so that
``torch.export`` does not emit a contiguity guard that would
prevent the ``nall == nloc`` (no-PBC) case from working.
For numpy / jax, uses regular slicing.
"""
if array_api_compat.is_torch_array(arr):
import torch
indices = torch.arange(n, dtype=torch.int64, device=arr.device)
return torch.index_select(arr, dim, indices)
slices = [slice(None)] * arr.ndim
slices[dim] = slice(0, n)
return arr[tuple(slices)]
def xp_scatter_sum(input: Array, dim: int, index: Array, src: Array) -> Array:
"""Reduces all values from the src tensor to the indices specified in the index tensor.
This function is similar to PyTorch's scatter_add and JAX's scatter_sum.
It adds values from src to input at positions specified by index along the given dimension.
"""
if array_api_compat.is_torch_array(input):
# PyTorch: use scatter_add (non-mutating version) for better performance
import torch
return torch.scatter_add(input, dim, index, src)
# Generic array_api implementation (works for JAX, NumPy, array-api-strict, etc.)
xp = array_api_compat.array_namespace(input)
if getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
import tensorflow as tf
input_tensor = input.unwrap()
index_tensor = tf.cast(index.unwrap(), tf.int64)
src_tensor = src.unwrap()
rank = input_tensor.shape.rank
if rank is None:
raise ValueError("xp_scatter_sum requires a statically known rank")
dim = dim + rank if dim < 0 else dim
src_shape = tf.shape(src_tensor, out_type=tf.int64)
coords = []
for axis in range(rank):
if axis == dim:
coord = index_tensor
else:
view_shape = [1] * rank
view_shape[axis] = src_shape[axis]
coord = tf.broadcast_to(
tf.reshape(tf.range(src_shape[axis], dtype=tf.int64), view_shape),
src_shape,
)
coords.append(coord)
scatter_indices = tf.reshape(tf.stack(coords, axis=-1), (-1, rank))
scatter_updates = tf.reshape(src_tensor, (-1,))
scattered = tf.scatter_nd(
scatter_indices,
scatter_updates,
tf.shape(input_tensor, out_type=tf.int64),
)
return xp.asarray(input_tensor + scattered)
# Create flat index array matching input shape
idx = xp.arange(input.size, dtype=xp.int64, device=array_api_compat.device(input))
idx = xp.reshape(idx, input.shape)
# Get flat indices where we want to add values
new_idx = xp_take_along_axis(idx, index, axis=dim)
new_idx = xp.reshape(new_idx, (-1,))
# Flatten arrays
shape = input.shape
input_flat = xp.reshape(input, (-1,))
src_flat = xp.reshape(src, (-1,))
# Add values at the specified indices
result = xp_add_at(input_flat, new_idx, src_flat)
# Reshape back to original shape
return xp.reshape(result, shape)
def xp_add_at(x: Array, indices: Array, values: Array) -> Array:
"""Adds values to the specified indices of x in place or returns new x (for JAX)."""
xp = array_api_compat.array_namespace(x, indices, values)
if array_api_compat.is_numpy_array(x):
# NumPy: supports np.add.at (in-place)
xp.add.at(x, indices, values)
return x
elif array_api_compat.is_jax_array(x):
# JAX: functional update, not in-place
return x.at[indices].add(values)
elif array_api_compat.is_torch_array(x):
# PyTorch: use index_add (non-mutating version)
import torch
return torch.index_add(x, 0, indices, values)
elif getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
import tensorflow as tf
x_tensor = x.unwrap()
indices_tensor = tf.reshape(tf.cast(indices.unwrap(), tf.int64), (-1, 1))
values_tensor = values.unwrap()
updates = tf.scatter_nd(
indices_tensor,
values_tensor,
tf.shape(x_tensor, out_type=tf.int64),
)
return xp.asarray(x_tensor + updates)
else:
# Fallback for array_api_strict: use basic indexing only
# may need a more efficient way to do this
n = indices.shape[0]
for i in range(n):
idx = int(indices[i])
x[idx, ...] = x[idx, ...] + values[i, ...]
return x
def xp_hint_dynamic_size(x: Array) -> None:
"""Mark a data-dependent leading dimension as a valid size for torch.export.
Under symbolic tracing (``make_fx`` / ``torch.export``) the length of a
data-dependent array (e.g. the output of ``nonzero`` or a tensor-``repeat``)
is an UNBACKED SymInt; guarding Python control flow or allocations on it
raises ``GuardOnDataDependentSymNode``. ``torch._check_is_size`` registers
the ``>= 0`` size hint that lets the tracer treat it as a proper dimension
(recorded as a ``sym_constrain_range_for_size`` node, preserved by AOTI).
No-op for numpy / jax / eager-torch concrete shapes — safe to call
unconditionally from dpmodel code (torch imported lazily, torch arrays only).
"""
if array_api_compat.is_torch_array(x):
import torch
torch._check_is_size(x.shape[0])
def xp_maximum_at(x: Array, indices: Array, values: Array) -> Array:
"""Segment max-assign of values into x at the specified indices.
Element-wise analogue of :func:`xp_add_at` that takes the maximum instead
of the sum: for every ``k`` it assigns ``x[indices[k]] = maximum(
x[indices[k]], values[k])``. Repeated indices reduce to the per-segment
maximum, which is order-independent.
Parameters
----------
x : Array
Destination array indexed along axis 0; typically pre-filled with
``-inf`` so empty segments stay neutral.
indices : Array
Integer destination indices with shape (K,).
values : Array
Source values with shape (K, *x.shape[1:]).
Returns
-------
Array
The updated array (modified in place and returned for NumPy; a new
array for JAX/PyTorch).
"""
xp = array_api_compat.array_namespace(x, indices, values)
if array_api_compat.is_numpy_array(x):
# NumPy: in-place ufunc reduction at the given indices.
xp.maximum.at(x, indices, values)
return x
elif array_api_compat.is_jax_array(x):
# JAX: functional indexed-max update, not in-place.
return x.at[indices].max(values)
elif array_api_compat.is_torch_array(x):
import torch
index = indices.reshape([-1] + [1] * (values.ndim - 1)).expand_as(values)
return torch.scatter_reduce(
x, 0, index, values, reduce="amax", include_self=True
)
elif getattr(xp, "__name__", "") == "deepmd._vendors.ndtensorflow":
import tensorflow as tf
x_tensor = x.unwrap()
indices_tensor = tf.reshape(tf.cast(indices.unwrap(), tf.int64), (-1,))
values_tensor = values.unwrap()
reduced = tf.math.unsorted_segment_max(
values_tensor,
indices_tensor,
tf.shape(x_tensor, out_type=tf.int64)[0],
)
segment_counts = tf.math.unsorted_segment_sum(
tf.ones_like(indices_tensor, dtype=tf.int32),
indices_tensor,
tf.shape(x_tensor, out_type=tf.int64)[0],
)
touched = segment_counts > 0
touched_shape = tf.concat(
[
tf.reshape(tf.shape(x_tensor, out_type=tf.int64)[0], (1,)),
tf.ones(tf.rank(x_tensor) - 1, dtype=tf.int64),
],
axis=0,
)
touched = tf.reshape(touched, touched_shape)
return xp.asarray(tf.where(touched, tf.maximum(x_tensor, reduced), x_tensor))
else:
# Fallback for array_api_strict: basic indexing only.
n = indices.shape[0]
for i in range(n):
idx = int(indices[i])
x[idx, ...] = xp.maximum(x[idx, ...], values[i, ...])
return x
def xp_sigmoid(x: Array) -> Array:
"""Compute the sigmoid function.
JAX and PyTorch have optimized sigmoid implementations.
See https://github.com/jax-ml/jax/discussions/15617
"""
if array_api_compat.is_jax_array(x):
from deepmd.jax.env import (
jax,
)
return jax.nn.sigmoid(x)
elif array_api_compat.is_torch_array(x):
import torch
return torch.sigmoid(x)
xp = array_api_compat.array_namespace(x)
return 1 / (1 + xp.exp(-x))
def xp_setitem_at(x: Array, mask: Array, values: Array) -> Array:
"""Set items at boolean mask indices.
For JAX and PyTorch arrays, returns a new array (non-mutating).
For NumPy arrays, modifies in-place and returns the same array.
Parameters
----------
x : Array
The array to modify
mask : Array
Boolean mask indicating positions to set
values : Array
Values to set at masked positions
Returns
-------
Array
Modified array (new array for JAX/PyTorch, same array for NumPy)
"""
if array_api_compat.is_jax_array(x):
# JAX doesn't support in-place item assignment
return x.at[mask].set(values)
elif array_api_compat.is_torch_array(x):
# PyTorch: clone to avoid mutating the input (non-mutating version)
import torch
result = torch.clone(x)
result[mask] = values
return result
# Standard item assignment for NumPy, array-api-strict, etc.
x[mask] = values
return x
def xp_bincount(x: Array, weights: Array | None = None, minlength: int = 0) -> Array:
"""Counts the number of occurrences of each value in x."""
xp = array_api_compat.array_namespace(x)
if (
array_api_compat.is_numpy_array(x)
or array_api_compat.is_jax_array(x)
or array_api_compat.is_torch_array(x)
):
result = xp.bincount(x, weights=weights, minlength=minlength)
else:
if weights is None:
weights = xp.ones_like(x)
result = xp.zeros(
(max(minlength, int(xp.max(x)) + 1),),
dtype=weights.dtype,
device=array_api_compat.device(weights),
)
result = xp_add_at(result, x, weights)
return result