Skip to content

[PROPOSAL] Creation of NDArray, Matrix views #279

@shivasankarka

Description

@shivasankarka

Summary

This proposal introduces a lightweight and extensible mechanism to create views of NDArray and Matrix objects in NuMojo, similar to the approach discussed in #169 , but without requiring Mojo support for trait parameters.

The goal is to enable array views as a first-class feature in NuMojo, allowing data sharing and view-based operations while minimizing unnecessary array copies. This lays the groundwork for improved memory efficiency as we continue to rework getter/setter methods and optimize internal data handling.

Motivation

In libraries like NumPy, array slicing and subsetting operations often return views instead of copies, enabling efficient reuse of existing memory. This significantly reduces overhead for large numerical computations.
With Mojo’s strong copy semantics now in place, NuMojo is well-positioned to adopt a similar design. Introducing views represents a key step toward zero-copy operations, allowing users to manipulate subarrays that share the same underlying data buffer.

notes

  • The example above illustrates one possible approach for constructing views of Matrix and NDArray. The specific names of the trait and struct types are open for discussion, they can be refined to better convey intent and improve clarity for future users and contributors.

  • Because this approach is non-intrusive and integrates cleanly with the current data model, it can be gradually incorporated into the existing NDArray and Matrix implementations in subsequent pull requests. Future updates to __getitem__ and __setitem__ will align these behaviors more closely with NumPy’s view semantics, ensuring consistency and intuitive usage.

Detailed Explanation

The proposed design adds a simple abstraction layer around view handling through a new Viewable trait and two supporting structs: ViewData and NonViewData that represent arrays views and original arrays. You can find the full example code here.

Trait definition

trait Viewable(ImplicitlyCopyable, Movable):
    fn __init__(out self):
        ...

    fn get(self) -> Bool:
        ...

ViewData and NonViewData

struct NonViewData(Viewable, ImplicitlyCopyable, Movable):
    alias view: Bool = False
    fn __init__(out self):
        pass

    fn get(self) -> Bool:
        return self.view


struct ViewData[is_mutable: Bool, //, origin: Origin[is_mutable]](Viewable, ImplicitlyCopyable, Movable):
    alias view: Bool = True
    fn __init__(out self):
        pass


    fn get(self) -> Bool:
        return self.view

Matrix

The Matrix type is extended with a View parameter that defaults to NonViewData.

struct Matrix[dtype: DType = DType.float64, View: Viewable = NonViewData](
    Copyable, Movable, Sized, Stringable, Writable
):
    alias width: Int = simd_width_of[dtype]()  #
    """Vector size of the data type."""

    var _buf: OwnData[dtype]
    """Data buffer of the items in the NDArray."""

    var view: View
    """View information of the NDArray."""

    var shape: Tuple[Int, Int]
    """Shape of Tensor."""

    var size: Int
    """Size of Tensor."""

    var strides: Tuple[Int, Int]
    """Strides of Tensor."""

    var flags: Flags
    "Information about the memory layout of the array."

This design enables both standard (owning) matrices and non-owning view matrices to coexist cleanly, distinguished by their View type parameter.

Constructing a View
A specialized constructor initializes a non-owning Matrix that references data from another array.

    @always_inline("nodebug")
    fn __init__(
        out self,
        shape: Tuple[Int, Int],
        strides: Tuple[Int, Int],
        offset: Int,
        ptr: UnsafePointer[Scalar[dtype]],
    ):
        self.shape = shape
        self.strides = strides
        self.size = shape[0] * shape[1]
        self._buf = OwnData(ptr=ptr.offset(offset))
        self.view = View()
        self.flags = Flags(
            self.shape, self.strides, owndata=False, writeable=False
        )

Example usage

from numojo.core.matrix_view import Matrix

fn main() raises:
    var a = Tensor(Tuple[Int, Int](2, 2))
    for i in range(4):
        a._store(i, val=i)
    print("a: ", a) 
    var b = a[1] # a[1] returns the view of row of array `a`. 
    print("b :", b)
    b._store(0, val=42) # modifies the data of array `a`
    print("b_modified: ", b)
    print("a_modified: ", a)

Possible naming conventions:

trait: Viewable, StorageKind, OwnerShip.
structs: Owning/NonOwning, Owning/View.
field: var ownership, var view.

Impact

Who benefits:

  1. Library users: Gain the ability to slice, reshape, and manipulate subarrays without memory duplication.
  2. NuMojo developers: Gain a clear and type-safe foundation for future view-based optimizations (e.g., lazy evaluation, broadcasting).
  3. Performance-critical applications: Reduced memory footprint and improved computational efficiency.

What changes:

  • Addition of Viewable, ViewData, and NonViewData types.
  • Extension of Matrix to support a generic view parameter.
  • New constructors for creating non-owning view matrices.

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions