Skip to content

Buffers, views, and memory

Example files: loom/docs/examples/elementwise-transform/

Loom separates storage identity from the logical shape used to access that storage. This keeps aliasing, memory-space, alignment, layout, and bounds facts available to the compiler instead of hiding them behind pointer arithmetic.

In this chapter, you will learn:

  • why kernels receive opaque buffer values and construct typed view values;
  • where byte offsets end and logical element coordinates begin;
  • how assumptions attach storage facts to new SSA values;
  • how scalar, vector, atomic, and masked accesses share one view model; and
  • how workgroup storage and synchronization remain explicit.

Storage identity and logical access are different contracts

Three value kinds meet at a memory operation:

Value What it owns What it does not own
buffer Opaque storage identity and root facts such as memory space, alignment, and alias scope. Element type, rank, shape, and address layout.
view A typed, non-owning logical coordinate space projected over a buffer. Allocation lifetime or independent storage identity.
vector or scalar The value transferred to or from storage. Address identity or a persistent attachment to memory.

This separation allows one allocation to have several useful projections. A packed weight buffer, for example, can expose byte payloads, integer words, and scale values through different views without pretending those views are different allocations.

Project a buffer into logical coordinates

buffer.view combines an opaque storage root, a physical base byte offset, and a result view type:

%base = index.constant 0 : offset
%input_view = buffer.view %input[%base] : buffer -> view<[%row_count]x[%column_count]xf32>

The operation neither allocates nor copies. %input_view refers to the same storage root as %input. Its two dimensions and f32 element type establish the logical coordinates accepted by later accesses.

The value between brackets on buffer.view is an offset measured in bytes. Indices on view.load, view.store, and vector memory operations are index values measured in logical elements:

%value = view.load %input_view[%row, %column] : view<[%row_count]x[%column_count]xf32> -> f32
view.store %value, %output_view[%row, %column] : f32, view<[%row_count]x[%column_count]xf32>

The full-rank index list names a position in view coordinates. An attached address layout, when present, maps that position to storage. Source operations do not reproduce the layout as flattened pointer arithmetic.

Cross the byte boundary once

Use index.scale when a logical coordinate selects a byte-addressed subregion:

%row_stride = index.constant 4096 : offset
%row_byte_offset = index.scale %first_row, %row_stride : index, offset -> offset
%rows = buffer.view %storage[%row_byte_offset] : buffer -> view<[%row_count]x1024xf32>

Once %rows exists, its operations use row and column indices. Multiplying every access by four or carrying a flattened byte pointer through the function would discard the distinction that lets Loom reason about bounds and layouts.

view.subview moves an origin in logical coordinates while retaining the same storage root and address layout:

%tile = view.subview %matrix[%row, %column] : view<[%m]x[%n]xf32> -> view<16x32xf32>

view.refine changes only the known shape or layout facts of the same view and byte base. Neither operation is an allocation or a data movement.

State storage facts at the boundary that knows them

External buffer parameters do not become independent merely because their SSA names differ. If the caller contract guarantees disjoint storage, make that guarantee visible:

%input_noalias, %output_noalias = buffer.assume.noalias %input, %output : buffer, buffer

Each assumption produces a refined SSA value over the same storage. Later uses must consume that result to benefit from the fact.

Operation Fact supplied by the author or embedding
buffer.assume.noalias The listed roots participate in comparable disjoint alias scopes.
buffer.assume.alignment Every listed root has at least the stated byte alignment.
buffer.assume.memory_space A root belongs to a concrete target-independent memory space.
buffer.assume.same_root Two handles are known to refer to the same underlying allocation.

Kernel ABI buffer arguments already carry their global-memory fact. Reusable functions that accept buffers outside that boundary may remain generic or use an explicit assumption when their caller contract knows more. An assumption is a correctness promise: supplying a false alias, alignment, extent, or memory-space fact makes the program invalid.

Transfer structured values through views

Scalar access names one logical element. vector.load and vector.store name a logical origin and transfer a vector footprint:

%values = vector.load %matrix[%row, %column] : view<[%m]x[%n]xf32> -> vector<4x8xf32>
vector.store %values, %result[%row, %column] : vector<4x8xf32>, view<[%m]x[%n]xf32>

Vector axes map to the trailing view axes. Leading view axes select the slice; the vector shape describes the transferred footprint. The source states the structured operation while target lowering chooses native instructions, splitting, or scalarization.

Masked operations make tail behavior part of the access itself. A false load lane does not access memory and takes its passthrough value; a false store lane does not modify memory:

%loaded = vector.load.mask %input[%row, %column], %mask, %zero : view<[%m]x[%n]xf32>, vector<8xi1>, vector<8xf32>
vector.store.mask %loaded, %output[%row, %column], %mask : vector<8xf32>, view<[%m]x[%n]xf32>, vector<8xi1>

Gather and scatter operations add per-lane logical offsets to the final view axis. Fragment loads and stores add a structured matrix-operand role. These are variations of the same view contract rather than separate pointer models.

Allocate scratch by physical extent

buffer.alloca creates a fresh scratch root in an allocatable memory space. Its requested extent and alignment are physical byte quantities:

%scratch_bytes = index.constant 4096 : offset
%base = index.constant 0 : offset
%scratch = buffer.alloca<workgroup> align(64) %scratch_bytes : buffer
%scratch_view = buffer.view %scratch[%base] : buffer -> view<32x32xf32>

Every execution produces a distinct storage identity. A target requiring a static frame reserves the proven finite maximum, and the compiled launch contract reports the total workgroup-local storage required by the kernel.

When several simultaneously live byte ranges share one slab, buffer.pack computes aligned, non-overlapping offsets and a repeatable total stride:

%total_bytes, %header_offset, %payload_offset = buffer.pack [align(16) %header_bytes, align(256) %payload_bytes] : offset

This is physical layout. Reusing one allocation across non-overlapping lifetimes is a compiler allocation decision and is not encoded by pretending the ranges are simultaneously live.

Synchronization names both rendezvous and memory

Storage visibility is not implied by source order across invocations. When workitems exchange values through workgroup storage, the program names the execution scope, fenced memory space, and ordering:

vector.store %values, %scratch_view[%row, %column] : vector<4xf32>, view<32x32xf32>
kernel.barrier<workgroup> scope(workgroup) ordering(acq_rel)
%transposed = vector.load %scratch_view[%column, %row] : view<32x32xf32> -> vector<4xf32>

kernel.barrier is a rendezvous for participating invocations plus a fence for the named memory space. Asynchronous transfers use kernel.async.group and kernel.async.wait for completion; a barrier is added only when invocations also need to rendezvous.

Atomic view operations similarly require an explicit scope and ordering. A plain store does not become atomic because several invocations may reach it, and a barrier does not resolve conflicting writes.

Preserve the strongest useful representation

Common representation mistakes all erase information too early:

Symptom Lost contract Stronger source shape
Every access performs byte arithmetic Logical shape and element stride Construct a view once, then index it logically.
Two parameters are assumed independent by convention Alias proof Refine them together with buffer.assume.noalias.
A flat view is used for a logical matrix Rank and axis structure Use a ranked view with the actual logical dimensions.
Tail lanes load first and mask later Memory safety Use a guarded region or masked memory operation.
Shared-memory communication relies on source order Cross-invocation visibility State the required barrier or async completion edge.
Target address spaces appear throughout a motif Reusability Use target-independent memory spaces and specialize at the leaf.

The source-to-artifacts kernel shows the complete buffer-to-view path with dynamic extent, explicit no-alias facts, a control-flow-derived in-bounds proof, and scalar access.

Continue with Kernels and launch configuration, where the storage operations become part of a dispatchable entry with separate workload and device contracts.