Expand description
§Memory Layout
A Layout is the descriptor that says how to address a tensor’s elements. It sits
alongside the buffer, so the operations that only change how elements are addressed -
reshape, slice, transpose, broadcast - rewrite the layout and copy nothing.
§The fields
shape - the logical dimensions, e.g. [3, 4] for a 3×4 matrix
stride - how many elements to skip per step in each dimension
adj_stride - how much the memory position changes when a dimension steps (see below)
offset - where in the buffer the tensor starts
len - total number of elements (product of shape)Element [i, j] of a 2D tensor sits at:
buffer[offset + i * stride[0] + j * stride[1]]Transposes, slices, and views are just different values of these fields over the same buffer.
§adj_stride - the iteration trick
adj_stride exists to remove the per-element multiply-and-accumulate that stride-based
addressing would otherwise cost. It models the gap between two consecutive elements at the
edge of a contiguous block: the jump the running position takes when an inner dimension
wraps and the next dimension out steps forward.
The naive way to iterate recomputes the full position from the multi-dimensional index at every element:
pos = offset + counter[0]*stride[0] + counter[1]*stride[1] + ...Moving from logical element N to N+1 only ever steps one dimension: the innermost one that
did not wrap. The delta added to pos depends on which dimension stepped, and
adj_stride[d] is that delta:
adj_stride[last] = stride[last]
adj_stride[d] = stride[d] − sum( stride[k] * (shape[k]−1) for k = d+1..last )Where the formula comes from. When dimension d increments, every inner dimension
(k > d) has just wrapped from shape[k]−1 back to zero - so the iterator has traveled
stride[k] * (shape[k]−1) forward through each one since d last stepped, all of which
must be undone. Then d steps forward once, adding stride[d]. The net delta is
stride[d] minus the walk through the inner dimensions. A negative component falls out of
this directly: it marks a dimension whose steps run backwards through memory.
Example: contiguous [3, 4], stride [4, 1]
adj_stride[1] = 1
adj_stride[0] = 4 − 1*(4−1) = 1Both are 1 - stepping any dimension moves by 1 from the previous position. This is the fast
path, and why adj_stride = [1, ..., 1] is hardcoded for freshly allocated tensors.
Example: transposed [4, 3] (from [3, 4], stride [1, 4])
adj_stride[1] = 4
adj_stride[0] = 1 − 4*(3−1) = −7adj_stride[1] = 4 skips 4 physical elements per inner step; adj_stride[0] = -7 jumps
backwards to the top of the next column when the outer dimension increments.
§Zero-copy operations
- View (reshape). New
shapeandstride, sameoffset, shared buffer. Requires a contiguous input - a transposed or sliced tensor must be packed withAsContiguousfirst. - Slice. Adjusts
offsetto the start of the subregion and updatesshape, keeping the original strides. - Transpose. Reverses
shapeandstrideand recomputesadj_stride; reversed axes give negativeadj_stridecomponents. - Permuting axes.
transpose_axesgeneralizes transpose to any axis permutation (and rejects non-bijections); it is the same shape/stride shuffle, no data moved.
§Broadcasting
Broadcasting lets a smaller tensor stand in for a larger one, and Candela implements it
entirely through zero strides. A dimension with stride = 0 reads the same physical offset
at every step along that axis. A [4] vector broadcast to [3, 4] gains a leading dimension
with stride = 0, so [i, j] reads buffer[i*0 + j*1] = buffer[j] - three rows over the
same four values, buffer untouched.
source: shape=[4], stride=[1], len=4 (physical buffer)
result: shape=[3,4], stride=[0, 1], len=12 (logical view)The right-aligned rules follow NumPy: dimensions beyond the source rank are prepended with
stride = 0, a source dimension of size 1 expands with stride = 0, and any other mismatch
is rejected.
The adj_stride formula needs no special case for a zero stride: it subtracts the inner
walk from zero, leaving a negative delta that resets the position to the start of the inner
sequence while the zero stride adds nothing - so the outer step restarts the inner row from
the same offset.
§Structural flags
Two cheap checks read straight off the layout instead of scanning the elements:
is_contiguous()isadj_stride[0] == 1together with no zero stride in the inner axes. The first part means every step advances by exactly one element; the zero-stride guard excludes broadcasts, whose repeated reads are not a contiguous run.is_transposed()is true when some axis hasadj_stride[d] < 0 && stride[d] != 0. Thestride != 0guard again excludes broadcast dimensions: a negativeadj_stridefrom a zero stride is the positional reset described above, not a reversal.
Together they let code decide whether a BLAS pack is needed without mistaking a broadcast for a transpose.