pulsatrix
Loading...
Searching...
No Matches
pulsatrix::Tensor Class Reference

N-dimensional tensor. Owns its data buffer exclusively; a DeviceBackend* is injected (not owned) – the backend must outlive every Tensor constructed against it, per cpp_style_guide/context_style_project_conventions.md's ownership table. More...

#include <tensor.hpp>

Public Member Functions

 Tensor (Shape shape, DeviceBackend *backend)
 Constructs a zero-initialized tensor on backend's own device (backend->device()).
 
 Tensor (Shape shape, DeviceBackend *backend, DeviceType device)
 Constructs a zero-initialized tensor with an explicit device tag.
 
 Tensor (Shape shape, DeviceBackend *backend, std::initializer_list< float > values, DeviceType device)
 Constructs a tensor from explicit values.
 
 Tensor (Shape shape, DeviceBackend *backend, std::initializer_list< float > values)
 As above, tagged with backend->device().
 
 Tensor (Shape shape, DeviceBackend *backend, const std::vector< float > &values, DeviceType device)
 Constructs a tensor from explicit values, runtime-sized source.
 
 Tensor (Shape shape, DeviceBackend *backend, const std::vector< float > &values)
 As above, tagged with backend->device().
 
 ~Tensor ()
 
 Tensor (const Tensor &other)
 Deep-copies another tensor's buffer.
 
Tensor & operator= (const Tensor &other)
 
 Tensor (Tensor &&other) noexcept
 
Tensor & operator= (Tensor &&other) noexcept
 
const Shape & shape () const
 This tensor's shape.
 
int64_t numel () const
 Total element count – shape().numel().
 
int64_t rank () const
 Number of dimensions – shape().rank().
 
DeviceType device () const
 Which device this tensor's buffer conceptually resides on.
 
DeviceBackend * backend () const
 The backend that owns this tensor's buffer. Not owned by the Tensor.
 
bool requires_grad () const
 Whether a module's backward() should accumulate a gradient for this tensor when it is a parameter, and whether optimizers should update it. Defaults to true.
 
void set_requires_grad (bool requires_grad)
 Sets requires_grad(); see there.
 
float read_element (int64_t flat_index) const
 Reads one element to the host through the owning backend, on any device.
 
std::vector< float > to_host_vector () const
 Copies the whole buffer to a host vector through the owning backend, on any device.
 
void write_element (int64_t flat_index, float value)
 Writes one element from the host through the owning backend, on any device.
 
const float * data () const
 Raw buffer access. nullptr iff numel() == 0.
 
float * data ()
 Raw buffer access (mutable). nullptr iff numel() == 0.
 
float & at (std::initializer_list< int64_t > index)
 Element access by multi-dimensional index (row-major).
 
const float & at (std::initializer_list< int64_t > index) const
 Const overload of at().
 
float & operator[] (int64_t flat_index)
 Flat (rank-agnostic) element access by linear offset into the row-major buffer.
 
const float & operator[] (int64_t flat_index) const
 Const overload of operator[].
 
Tensor & fill (float value)
 Sets every element to value. Safe no-op on a zero-element tensor.
 
Tensor & accumulate (const Tensor &other)
 In-place elementwise accumulation: this[i] += other[i] for every element.
 
Tensor & reshape (Shape new_shape)
 Reinterprets this tensor's dimensions in place – same buffer, new shape.
 
Tensor & to (DeviceType target)
 Same-device no-op form of to().
 
Tensor & to (DeviceType target, DeviceBackend *target_backend)
 Moves this tensor's buffer to another device, owned by target_backend.
 

Static Public Member Functions

static Tensor Stack (const std::vector< Tensor > &tensors, DeviceBackend *backend)
 Concatenates N tensors along their leading dimension into one batch Tensor – pulsatrix's collate-time primitive (campaign_exai_dl_library_data_pipeline, Mission 0). E.g. stacking three (1, 28, 28) MNIST-style per-sample images produces one (3, 28, 28) batch.
 

Detailed Description

N-dimensional tensor. Owns its data buffer exclusively; a DeviceBackend* is injected (not owned) – the backend must outlive every Tensor constructed against it, per cpp_style_guide/context_style_project_conventions.md's ownership table.

Note
Invariant: data() == nullptr iff numel() == 0. A zero-element Tensor never allocates (matches DeviceBackend::allocate()'s zero-byte-returns-nullptr convention) – this is not an error state, it's the expected representation of an empty tensor.

Constructor & Destructor Documentation

◆ Tensor() [1/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend 
)

Constructs a zero-initialized tensor on backend's own device (backend->device()).

Parameters
shapeTensor shape.
backendBackend to allocate/fill through. Not owned; must outlive this Tensor.

◆ Tensor() [2/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend,
DeviceType  device 
)

Constructs a zero-initialized tensor with an explicit device tag.

Parameters
shapeTensor shape.
backendBackend to allocate/fill through. Not owned; must outlive this Tensor.
deviceWhich device this tensor's buffer conceptually resides on. Should equal backend->device(); deliberately not enforced, because tests tag a host-backed buffer Cuda/Hip to exercise device guards without GPU hardware.

◆ Tensor() [3/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend,
std::initializer_list< float >  values,
DeviceType  device 
)

Constructs a tensor from explicit values.

Parameters
shapeTensor shape. values.size() must equal shape.numel().
backendBackend to allocate/copy through.
valuesInitial values, in row-major order.
deviceWhich device this tensor's buffer conceptually resides on.
Note
Copies via CopyDirection::HostToHost if device is Cpu, else HostToDevice – values.begin() is always a genuine host pointer (std::initializer_list lives on the host) regardless of the destination.

◆ Tensor() [4/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend,
std::initializer_list< float >  values 
)

As above, tagged with backend->device().

◆ Tensor() [5/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend,
const std::vector< float > &  values,
DeviceType  device 
)

Constructs a tensor from explicit values, runtime-sized source.

Parameters
shapeTensor shape. values.size() must equal shape.numel().
backendBackend to allocate/copy through.
valuesInitial values, in row-major order.
deviceWhich device this tensor's buffer conceptually resides on.
Note
Same semantics as the std::initializer_list overload above – exists because std::initializer_list has no portable public constructor from a runtime-sized buffer (pointer + size), so any caller with data whose size isn't known at the call site (loading weights from a file, marshalling a numpy array across the Phase 5 Python bindings) cannot use the initializer_list overload at all, not just less conveniently.

◆ Tensor() [6/8]

pulsatrix::Tensor::Tensor ( Shape  shape,
DeviceBackend *  backend,
const std::vector< float > &  values 
)

As above, tagged with backend->device().

◆ ~Tensor()

pulsatrix::Tensor::~Tensor ( )

◆ Tensor() [7/8]

pulsatrix::Tensor::Tensor ( const Tensor &  other)

Deep-copies another tensor's buffer.

Note
Copies via CopyDirection::HostToHost if device() is Cpu, else DeviceToDevice – both this tensor's and other's buffers live on the same device, since both are allocated by the same backend_.

◆ Tensor() [8/8]

pulsatrix::Tensor::Tensor ( Tensor &&  other)
noexcept

Member Function Documentation

◆ accumulate()

Tensor & pulsatrix::Tensor::accumulate ( const Tensor &  other)

In-place elementwise accumulation: this[i] += other[i] for every element.

Parameters
otherTensor to add into this one. Must have the same shape.
Returns
*this, for chaining (e.g. grad.accumulate(a).accumulate(b)).
Note
Distinct from a general-purpose arithmetic operator+ – that remains deferred (see Mission 0/1 AAR) until Tensor's broader math API is designed in Phase 1. This method exists specifically for gradient accumulation (Mission 3 autograd), where the in-place, same-shape-only semantics are exactly what's needed and nothing more.

◆ at() [1/2]

float & pulsatrix::Tensor::at ( std::initializer_list< int64_t >  index)

Element access by multi-dimensional index (row-major).

Parameters
indexOne index per dimension; index.size() must equal rank().
Returns
Reference to the element.
Note
Bounds and rank are checked via PULSATRIX_ASSERT (programmer-error contract, not a condition a well-formed caller can legitimately trigger) – see cpp_style_guide/context_style_project_conventions.md's assert-vs-throw table. Assumes a host-addressable (Cpu) backend: on a Cuda/Hip Tensor, move it with to(DeviceType::Cpu, cpu_backend) first.

◆ at() [2/2]

const float & pulsatrix::Tensor::at ( std::initializer_list< int64_t >  index) const

Const overload of at().

◆ backend()

DeviceBackend * pulsatrix::Tensor::backend ( ) const
inline

The backend that owns this tensor's buffer. Not owned by the Tensor.

Note
Lets device-generic code (e.g. an optimizer stepping a module's parameters) compute through the buffer's own backend instead of carrying a second pointer that could name a different device.

◆ data() [1/2]

float * pulsatrix::Tensor::data ( )
inline

Raw buffer access (mutable). nullptr iff numel() == 0.

◆ data() [2/2]

const float * pulsatrix::Tensor::data ( ) const
inline

Raw buffer access. nullptr iff numel() == 0.

◆ device()

DeviceType pulsatrix::Tensor::device ( ) const
inline

Which device this tensor's buffer conceptually resides on.

◆ fill()

Tensor & pulsatrix::Tensor::fill ( float  value)

Sets every element to value. Safe no-op on a zero-element tensor.

Parameters
valueFill value.
Returns
*this, for chaining (e.g. t.fill(0.0f).fill_diagonal(1.0f)).

◆ numel()

int64_t pulsatrix::Tensor::numel ( ) const
inline

Total element count – shape().numel().

◆ operator=() [1/2]

Tensor & pulsatrix::Tensor::operator= ( const Tensor &  other)

◆ operator=() [2/2]

Tensor & pulsatrix::Tensor::operator= ( Tensor &&  other)
noexcept

◆ operator[]() [1/2]

float & pulsatrix::Tensor::operator[] ( int64_t  flat_index)
inline

Flat (rank-agnostic) element access by linear offset into the row-major buffer.

Note
PULSATRIX_ASSERT-gated bounds check, not throw – internal invariant per campaign_exai_dl_library_adversarial_hardening.md's Mission 0 classification: this is a hot path called internally (CPUBackend loops, module forward/backward) with an already-computed, already-valid index, never directly from unvalidated external input.

◆ operator[]() [2/2]

const float & pulsatrix::Tensor::operator[] ( int64_t  flat_index) const
inline

Const overload of operator[].

◆ rank()

int64_t pulsatrix::Tensor::rank ( ) const
inline

Number of dimensions – shape().rank().

◆ read_element()

float pulsatrix::Tensor::read_element ( int64_t  flat_index) const

Reads one element to the host through the owning backend, on any device.

Note
One synchronous device-to-host copy – for scalars (a loss value, a picked logit), never for loops. Bounds are PULSATRIX_ASSERT-checked like operator[].

◆ requires_grad()

bool pulsatrix::Tensor::requires_grad ( ) const
inline

Whether a module's backward() should accumulate a gradient for this tensor when it is a parameter, and whether optimizers should update it. Defaults to true.

Note
Meaningful only for parameters (roadmap FND-2); ignored everywhere else.
The flag belongs to the object, not its values: copy and move construction carry it over, but copy and move assignment keep the destination's own flag. Loading new weights into a frozen parameter (weight_ = loaded;) therefore leaves it frozen, matching PyTorch's param.data = x.

◆ reshape()

Tensor & pulsatrix::Tensor::reshape ( Shape  new_shape)

Reinterprets this tensor's dimensions in place – same buffer, new shape.

Parameters
new_shapeTarget shape. Must have the same numel() as the current shape.
Returns
*this, for chaining.
Exceptions
std::invalid_argumentif new_shape.numel() != numel().

◆ set_requires_grad()

void pulsatrix::Tensor::set_requires_grad ( bool  requires_grad)
inline

Sets requires_grad(); see there.

◆ shape()

const Shape & pulsatrix::Tensor::shape ( ) const
inline

This tensor's shape.

◆ Stack()

static Tensor pulsatrix::Tensor::Stack ( const std::vector< Tensor > &  tensors,
DeviceBackend *  backend 
)
static

Concatenates N tensors along their leading dimension into one batch Tensor – pulsatrix's collate-time primitive (campaign_exai_dl_library_data_pipeline, Mission 0). E.g. stacking three (1, 28, 28) MNIST-style per-sample images produces one (3, 28, 28) batch.

Parameters
tensorsNon-empty list of tensors, each rank >= 1, each on the same device, all identical in every dimension except the leading one.
backendBackend to allocate the output buffer through. Not owned.
Returns
A new Tensor whose leading dimension is the sum of every input tensor's leading dimension, and whose remaining dimensions match the inputs'.
Exceptions
std::invalid_argumentif tensors is empty, any tensor has rank 0, ranks differ across tensors, non-leading dimensions differ across tensors, or devices differ across tensors – external boundary: the list of tensors to stack is assembled by a DataLoader/collate function from independently constructed Dataset samples, not a compile-time-known invariant.

◆ to() [1/2]

Tensor & pulsatrix::Tensor::to ( DeviceType  target)

Same-device no-op form of to().

Parameters
targetTarget device. Must equal device().
Returns
*this, for chaining.
Note
A Tensor holds exactly one non-owned DeviceBackend*, and there is no global backend registry, so a cross-device move cannot know which backend should own the new buffer – use to(target, target_backend) for that. This overload exists for the charter's Phase 0 exit gate (to(device()) is a no-op).
Exceptions
std::invalid_argumentif target != device().

◆ to() [2/2]

Tensor & pulsatrix::Tensor::to ( DeviceType  target,
DeviceBackend *  target_backend 
)

Moves this tensor's buffer to another device, owned by target_backend.

Parameters
targetDevice the new buffer resides on. Must be the device target_backend allocates on (Cpu for CPUBackend, Cuda for CUDABackend, Hip for HIPBackend) – not checkable here, since DeviceBackend does not report its own device.
target_backendBackend to allocate the new buffer through. Not owned; must outlive this Tensor, exactly as the constructor's backend must.
Returns
*this, for chaining. Afterwards device() == target and every subsequent allocate/copy/free goes through target_backend.
Note
The copy is issued by whichever backend owns the device-side pointer: Cpu -> device uses target_backend (HostToDevice); device -> Cpu uses the current backend (DeviceToHost); between two different GPU device types (Cuda <-> Hip) the data is staged through a host buffer, since neither vendor's runtime can address the other's memory. Same device type through a different backend instance copies directly (HostToHost / DeviceToDevice).
Strong exception guarantee: if allocation or the copy throws, this Tensor is left unchanged (same buffer, backend and device) and the new buffer is released.
A no-op when target == device() and target_backend is the current backend.
Exceptions
std::invalid_argumentif target_backend is nullptr.

◆ to_host_vector()

std::vector< float > pulsatrix::Tensor::to_host_vector ( ) const

Copies the whole buffer to a host vector through the owning backend, on any device.

Returns
numel() floats in row-major order (empty for a zero-element tensor).
Note
One synchronous copy (HostToHost on Cpu, DeviceToHost otherwise) – the explicit transfer a deliberate host boundary (an environment, a replay buffer, an agent's action selection; GPU-native-kernels Mission 7) uses to read a possibly-device tensor once, instead of dereferencing data() in a host loop.

◆ write_element()

void pulsatrix::Tensor::write_element ( int64_t  flat_index,
float  value 
)

Writes one element from the host through the owning backend, on any device.


The documentation for this class was generated from the following file: