Tensor Indexing and Slicing#
NKIPy tensors support a subset of NumPy’s indexing and slicing operations, optimized for machine learning workloads.
Supported Operations#
Operation |
Example |
Description |
|---|---|---|
Basic slicing |
|
Slice with start:stop |
Step slicing |
|
Every nth element |
Negative slicing |
|
Negative bounds |
Negative indexing |
|
Single negative indices |
Array indexing |
|
Index with array (single axis) |
List indexing |
|
Index with Python list |
Mixed indexing |
|
Static slice + dynamic index |
Scalar indexing |
|
Use 0D tensor as index |
Assignment |
|
Slice assignment |
Chained slicing |
|
Multiple slice operations |
Ellipsis |
|
Auto-expand |
newaxis/None |
|
Insert size-1 dimension |
Unsupported Operations#
Operation |
Example |
Workaround |
|---|---|---|
Multiple dynamic indices |
|
Use separate operations |
Boolean indexing |
|
Use |
Limitations#
Single dynamic index: Only one tensor/array index per operation is supported. For multiple dynamic indices, use separate operations:
# ❌ Not supported
tensor[batch_idx, seq_idx, :]
# ✅ Use separate operations
batch_selected = tensor[batch_idx, :, :]
result = batch_selected[:, seq_idx, :]
Examples#
Batch Selection#
def select_batches(tensor, batch_indices):
return tensor[batch_indices, :, :]
# Usage
batch_tensor = np.random.randn(8, 32, 64) # [batch, seq, hidden]
indices = np.array([0, 2, 4, 6])
result = select_batches(batch_tensor, indices) # Shape: (4, 32, 64)
Token Selection#
def select_tokens(sequences, token_positions):
return sequences[:, token_positions, :]
# Usage
sequences = np.random.randn(4, 128, 768) # [batch, seq_len, hidden]
positions = np.array([0, 10, 20, 30])
result = select_tokens(sequences, positions) # Shape: (4, 4, 768)
Embedding Lookup#
def embedding_lookup(embeddings, token_ids):
flat_tokens = np.reshape(token_ids, (-1,))
selected = embeddings[flat_tokens]
batch_size, seq_len = token_ids.shape
return np.reshape(selected, (batch_size, seq_len, embeddings.shape[1]))
# Usage
embeddings = np.random.randn(10000, 512) # Vocabulary embeddings
token_ids = np.random.randint(0, 10000, (8, 64))
result = embedding_lookup(embeddings, token_ids) # Shape: (8, 64, 512)
Scalar Tensor Indexing#
def select_expert(top_k_indices, expert_weights):
expert_idx = top_k_indices[0] # 0D tensor (scalar)
return expert_weights[expert_idx]
# Usage - Mixture of Experts routing
top_k_indices = np.array([1, 0, 2], dtype=np.int32)
expert_weights = np.array([[0.8, 0.6], [0.4, 0.7], [0.5, 0.9]])
result = select_expert(top_k_indices, expert_weights) # Shape: (2,)
Ellipsis Indexing#
# Ellipsis (...) expands to as many full slices as needed to cover
# all remaining dimensions. Useful when the number of leading
# dimensions varies.
tensor = np.random.randn(4, 8, 12) # [batch, seq, hidden]
# These are equivalent:
result = tensor[..., 0:3] # last dim slice
result = tensor[:, :, 0:3] # explicit full slices
# Leading integer + ellipsis:
result = tensor[0, ...] # first batch, all remaining dims
# Middle ellipsis:
result = tensor[0, ..., 0:3] # first batch, last dim slice
Newaxis (None) Indexing#
# None (np.newaxis) inserts a size-1 dimension at the specified position.
# This is useful for broadcasting in ML computations.
tensor = np.random.randn(8, 16) # [rows, cols]
# Add dimension in the middle:
result = tensor[:, None, :] # Shape: (8, 1, 16)
# Add leading dimension:
result = tensor[None, :, :] # Shape: (1, 8, 16)
# Add trailing dimension:
result = tensor[:, :, None] # Shape: (8, 16, 1)
# Multiple newaxis insertions:
result = tensor[None, :, None, :, None] # Shape: (1, 8, 1, 16, 1)
# Combined with integer indexing:
result = tensor[0, None, :] # Shape: (1, 16)
# Combined with ellipsis:
tensor_3d = np.random.randn(4, 8, 12)
result = tensor_3d[None, ..., None] # Shape: (1, 4, 8, 12, 1)
# Note: newaxis in assignment (setitem) is not supported.
# Use np.expand_dims() on the value instead.
View Aliasing Limitation#
Mutations through views are not tracked by NKIPy’s alias detection. If you extract a sub-tensor with __getitem__ and then mutate it, the change will not propagate back to the original tensor.
# ❌ Mutation through a view — NOT detected
b = a[0] # b is a new tensor, not a view of a
b[0:3] = value # only b is mutated; a is unchanged
# ✅ Mutate the original tensor directly
a[0, 0:3] = value
This is a consequence of NKIPy’s SSA-based computation graph: __getitem__ creates a new NKIPyTensorRef with no link back to its parent. This is consistent with the behavior of JAX and XLA.