Skip to content

Commit 93cad7f

Browse files
committed
data: Make local indexing O(result size), not O(domain)
Two hot-path costs in serial/local data[idx], surfaced by benchmarking: * index_decomposition (used by every basic slice) built a full np.arange over the axis and ran np.isin against each whole subdomain -- O(domain) per slice. Subdomains are contiguous ranges, so bucket the selected indices by bounds in O(result size); an np.isin fallback covers any non-contiguous subdomain. A 1024-wide slice of a 2**20 axis drops from ~2.1 ms to ~20 us. * convert_index ran index_glb_to_loc per element via np.vectorize for an array index. For a single (non-distributed) subdomain the map is just a negative-index wrap, so vectorize it; the distributed path (handled by the redistribution engine) is unchanged. A 65536-point assignment drops from ~62 ms to ~1.6 ms. Both are behaviour-preserving (doctests, TestDecomposition, and the mode-4 slicing/advanced-indexing tests pass).
1 parent 9cb66bc commit 93cad7f

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

devito/data/decomposition.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,27 @@ def index_decomposition(self, idx):
592592
return Decomposition([np.array([], dtype=np.int64)]*len(self),
593593
self.local)
594594
size = self.glb_max - self.glb_min + 1
595-
selected = np.arange(self.glb_min, self.glb_min + size)[idx]
596-
items = [np.nonzero(np.isin(selected, sd))[0] for sd in self]
595+
596+
# The global indices selected by `idx`, in result order. For a slice
597+
# (the only caller) this is built directly, without materialising the
598+
# whole axis.
599+
if isinstance(idx, slice):
600+
start, stop, step = idx.indices(size)
601+
selected = self.glb_min + np.arange(start, stop, step)
602+
else:
603+
selected = np.arange(self.glb_min, self.glb_min + size)[idx]
604+
605+
# Bucket the selected indices by owning subdomain. A subdomain is a
606+
# contiguous range, so membership is a bounds check in O(len(selected))
607+
# -- avoiding an O(axis) `isin` on every slice. A non-contiguous
608+
# subdomain (not produced by gridding/slicing) falls back to `isin`.
609+
items = []
610+
for sd in self:
611+
if sd.size == 0:
612+
items.append(np.array([], dtype=np.int64))
613+
elif sd[-1] - sd[0] + 1 == sd.size:
614+
owned = (selected >= sd[0]) & (selected <= sd[-1])
615+
items.append(np.nonzero(owned)[0])
616+
else:
617+
items.append(np.nonzero(np.isin(selected, sd))[0])
597618
return Decomposition(items, self.local)

devito/data/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ def convert_index(idx, decomposition, mode='glb_to_loc'):
100100
elif isinstance(idx, (tuple, list)):
101101
return [decomposition(i, mode=mode) for i in idx]
102102
elif isinstance(idx, np.ndarray):
103+
if mode == 'glb_to_loc' and len(decomposition) == 1 \
104+
and not decomposition.loc_empty:
105+
# A single (non-distributed) subdomain holds the whole axis, so the
106+
# global-to-local map is just a negative-index wrap. Vectorize it
107+
# instead of calling `index_glb_to_loc` per element via np.vectorize.
108+
n = decomposition.glb_max - decomposition.glb_min + 1
109+
return np.where(idx < 0, idx + n, idx).astype(idx.dtype)
103110
return np.vectorize(lambda i: decomposition(i, mode=mode))(idx).astype(idx.dtype)
104111
else:
105112
raise ValueError(f"Cannot convert index of type `{type(idx)}` ")

0 commit comments

Comments
 (0)