Skip to content

Commit bd53967

Browse files
dennisYatuninclaude
andcommitted
Bound Larcform1 compile with folded flat DataLayout indexing [perf]
The unified DataLayouts back each layout with a 5-D parent array (and property views with SubArrays of it). The finite-difference stencil getidx replicates a point read 3^depth times; on the branch each read carried Base's full 5-D index-decomposition (a plain Array's runtime col-major arithmetic, or a property-view SubArray's linear-to-Cartesian reindex), so the deeply-inlined stencil expression body exploded and Larcform1 compile-killed (OOM). Add a constant-folded flat leaf-access path. `flat_mode(data)` is a pure type function (Val(0/1/2), folds away) selecting: Val(1) IndexLinear parent -> affine index into the parent (native arrayref). Val(2) shape-linear IndexCartesian property view (the column case) -> reduce to the root Array with a folded affine index (one hoistable first(indices) load), instantiating no Base reindex / div / rem. Val(0) everything else (dynamic Nh, box VIJFH view Nh>1) -> unchanged Cartesian get_struct fallback. get_struct_linear/set_struct_linear! (+ a linear bitcast_struct_linear reusing bitcast_struct_expr) address the Nf entries at base + (f-1)*fstride. Measured (EDMF Larcform1 staged, interleaved): implicit_tendency! compile 32-33.5s vs 108-110s on the branch (~main's ~30-38s); peak RSS lowest of all. Column FD-op runtime improves (e.g. GradientC2F 75.5->47.5 ns, div_interp 188->111 ns). Bit-identical values across scalar/multi-component, VIJFH/VIJHF, Nh=1/Nh>1, property views, and setindex round-trip; device :kernel compile unchanged (GPU-safe). Builds on the Slice-based property views (fast-linear VIJHF) from the previous commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dd20050 commit bd53967

3 files changed

Lines changed: 219 additions & 2 deletions

File tree

src/DataLayouts/bitcast_struct.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
field_expr(f, value_expr) = :(Core.getfield($value_expr, $f))
33
field_expr(f, array_expr, index_expr) =
44
:(@inbounds $array_expr[struct_index($f, $array_expr, $index_expr...)])
5+
# Flat/linear variant (see get_struct_linear): read field f of the value stored
6+
# at linear position `base` using a single linear load into the array's flat
7+
# storage. `base` and `fstride` are already fully constant-folded affine
8+
# expressions of the layout's static shape params, and `$(f - 1)` is a literal,
9+
# so `array[base + (f - 1) * fstride]` lowers to one arrayref with a precomputed
10+
# offset -- no Cartesian reindex, no div/rem, no per-dimension bounds math.
11+
field_expr(f, array_expr, base_expr, fstride_expr) =
12+
:(@inbounds $array_expr[$base_expr + $(f - 1) * $fstride_expr])
513

614
# Keep array element read instructions separate unless the full value is needed
715
full_value_expr(@nospecialize(S), value_expr) = value_expr
@@ -155,3 +163,22 @@ For more information about `reinterpret` and padding, see the following:
155163
S = NTuple{num_indices, eltype(array)}
156164
return Expr(:block, :@inline, bitcast_struct_expr(T, S, :array, :index))
157165
end
166+
167+
# Flat/linear analogue of the array method (see get_struct_linear): reads the
168+
# `num_indices` entries at linear positions `base, base + fstride, ...` using the
169+
# flat `field_expr(f, array, base, fstride)`. Reuses bitcast_struct_expr so the
170+
# type-reconstruction machinery is shared with the Cartesian path.
171+
@generated function bitcast_struct_linear(
172+
::Type{T},
173+
array,
174+
::Val{num_indices},
175+
base,
176+
fstride,
177+
) where {T, num_indices}
178+
S = NTuple{num_indices, eltype(array)}
179+
return Expr(
180+
:block,
181+
:@inline,
182+
bitcast_struct_expr(T, S, :array, :base, :fstride),
183+
)
184+
end

src/DataLayouts/indexing.jl

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,159 @@ is_invalid_linear(data, index) = index isa Integer && IndexStyle(data) isa Index
7070
# level) are identified by their length, since they keep their dimensions.
7171
is_trivial_point(data, index) = isone(length(data)) && index == CartesianIndex()
7272

73+
# ---------------------------------------------------------------------------
74+
# Flat (constant-folded linear) leaf access.
75+
#
76+
# A point access reads `ncomponents(data)` entries out of the parent buffer. When
77+
# the parent has `IndexLinear` storage, `getindex(data, index)` used to route
78+
# through the Cartesian `get_struct`, which reads `parent[CartesianIndex(...)]`.
79+
# For a 5-D `Array` that generates the full 5-D linear-index arithmetic per tap,
80+
# and for a property-view `SubArray` it generates Base's linear-to-Cartesian
81+
# reindex -- the residual per-access bloat that inflates deeply-inlined stencil
82+
# expression bodies (and, for `SubArray`s, costs `div`/`rem` at runtime).
83+
#
84+
# `flat_mode(data)` picks one of three fully-static leaf-access strategies (the
85+
# `Val` is a pure function of the type, so the branch folds away and only the
86+
# selected `_{get,set}idx_flat` method is compiled per concrete layout):
87+
#
88+
# Val(1) parent is `IndexLinear` and every extent is inferable: index the
89+
# parent directly with a constant-folded affine index. For a plain
90+
# `Array` this is a native `arrayref`; for a fast property-view
91+
# `SubArray` (e.g. `VIJHF`, F trailing) Base lowers `sub[i::Int]` to
92+
# `parent(sub)[offset1 + i]` -- a single load into the root buffer.
93+
# Val(2) parent is an `IndexCartesian` property-view `SubArray` (e.g. a
94+
# `VIJFH` field view, F not trailing) but the layout is shape-linear
95+
# (all logical extents from the F axis onward are 1, the column case):
96+
# reduce to the root `Array` directly with an affine index that is
97+
# fully folded except for one hoistable `first(indices)` load, so no
98+
# Base reindex / `div` / `rem` is instantiated.
99+
# Val(0) everything else (dynamic `Nh`, or a true box `VIJFH` field view with
100+
# `Nh > 1`): the unchanged Cartesian path, which stays correct.
101+
@inline function flat_mode(data::DataLayout)
102+
has_inferred_size(data) || return Val(0)
103+
IndexStyle(data) isa IndexLinear && return Val(1)
104+
F = f_dim(data)
105+
F isa Integer || return Val(0)
106+
# The root path omits every logical coordinate at or beyond the F axis, so it
107+
# is only correct when those extents are all 1 (the column case). Unlike the
108+
# layout IndexStyle, a `ncomponents <= 1` escape is NOT valid here: a scalar
109+
# field view of a box (Nh > 1) still needs its h-term, which we drop.
110+
all_ones(inferred_size(data)[F:end]...) &&
111+
reducible_property_view(parent_type(data), Val(F)) && return Val(2)
112+
return Val(0)
113+
end
114+
115+
# A property-view SubArray (as built by struct_field_view) of an IndexLinear
116+
# array, whose indices are full `Slice`s except for a `UnitRange` at position F.
117+
# Reducing such a view to its root is a constant-stride affine map, so the flat
118+
# path can bypass Base's linear-to-Cartesian reindex.
119+
@inline reducible_property_view(::Type, ::Val) = false
120+
@inline reducible_property_view(
121+
::Type{<:SubArray{<:Any, N, PA, I}},
122+
::Val{F},
123+
) where {N, PA, I, F} =
124+
F isa Integer && 1 <= F <= N && IndexStyle(PA) isa IndexLinear ?
125+
_pv_indices_ok(I, Val(F), Val(N)) : false
126+
@generated function _pv_indices_ok(::Type{I}, ::Val{F}, ::Val{N}) where {I, F, N}
127+
for k in 1:N
128+
ft = fieldtype(I, k)
129+
ok = k == F ? (ft <: AbstractUnitRange && !(ft <: Base.Slice)) : (ft <: Base.Slice)
130+
ok || return :(false)
131+
end
132+
return :(true)
133+
end
134+
135+
# Column-major linear offset (0-based) of coordinate tuple `idx` in an array of
136+
# static size `sz`. Tail recursion over tuples inlines to a folded Horner form;
137+
# `sz` entries are type-domain literals, only the coordinates are runtime values.
138+
@inline _col_major_offset(::Tuple{}, ::Tuple{}) = 0
139+
@inline _col_major_offset(sz::Tuple, idx::Tuple) =
140+
(idx[1] - 1) + sz[1] * _col_major_offset(Base.tail(sz), Base.tail(idx))
141+
142+
@inline _first_n(t::Tuple, ::Val{n}) where {n} = ntuple(k -> t[k], Val(n))
143+
144+
# (base, fstride) for the Val(1) parent path. `base` is the linear position of
145+
# the first component of point `index`; `fstride` is the spacing between
146+
# successive components. For IndexLinear layouts the components form contiguous
147+
# length-`prod(inferred_size)` blocks, so `fstride == prod(inferred_size(data))`
148+
# and a linear point index maps to itself.
149+
@inline _flat_point_offset(data::DataLayout, index::Integer) =
150+
(index, prod(inferred_size(data)))
151+
@inline _flat_point_offset(data::DataLayout, index::CartesianIndex) =
152+
(_col_major_offset(inferred_size(data), Tuple(index)) + 1, prod(inferred_size(data)))
153+
154+
# (root, base, fstride) for the Val(2) property-view root path. `fstart` (the
155+
# start of the field range) is the only runtime input; everything else folds
156+
# from the static shape. Shape-linearity guarantees every logical coordinate at
157+
# or beyond the F axis is 1, so only the `F - 1` leading dims contribute.
158+
@inline function _flat_root_offset(
159+
data::DataLayout,
160+
index::CartesianIndex,
161+
fstart,
162+
::Val{F},
163+
) where {F}
164+
before_sz = _first_n(inferred_size(data), Val(F - 1))
165+
before_idx = _first_n(Tuple(index), Val(F - 1))
166+
fstride = prod(before_sz) # root stride of the F axis
167+
base = _col_major_offset(before_sz, before_idx) + (fstart - 1) * fstride + 1
168+
return (base, fstride)
169+
end
170+
@inline _root_and_fstart(data::DataLayout, ::Val{F}) where {F} =
171+
(parent(parent(data)), first(parentindices(parent(data))[F]))
172+
173+
@propagate_inbounds _getidx_flat(::Val{0}, data, index) =
174+
get_struct(parent(data), eltype(data), index, Val(f_dim(data)))
175+
@propagate_inbounds function _getidx_flat(::Val{1}, data, index)
176+
(base, fstride) = _flat_point_offset(data, index)
177+
return get_struct_linear(
178+
parent(data),
179+
eltype(data),
180+
base,
181+
fstride,
182+
Val(ncomponents(data)),
183+
)
184+
end
185+
@propagate_inbounds function _getidx_flat(::Val{2}, data, index)
186+
(root, fstart) = _root_and_fstart(data, Val(f_dim(data)))
187+
(base, fstride) = _flat_root_offset(data, index, fstart, Val(f_dim(data)))
188+
return get_struct_linear(root, eltype(data), base, fstride, Val(ncomponents(data)))
189+
end
190+
191+
@propagate_inbounds _setidx_flat!(::Val{0}, data, value, index) =
192+
set_struct!(parent(data), convert(eltype(data), value), index, Val(f_dim(data)))
193+
@propagate_inbounds function _setidx_flat!(::Val{1}, data, value, index)
194+
(base, fstride) = _flat_point_offset(data, index)
195+
return set_struct_linear!(
196+
parent(data),
197+
convert(eltype(data), value),
198+
base,
199+
fstride,
200+
Val(ncomponents(data)),
201+
)
202+
end
203+
@propagate_inbounds function _setidx_flat!(::Val{2}, data, value, index)
204+
(root, fstart) = _root_and_fstart(data, Val(f_dim(data)))
205+
(base, fstride) = _flat_root_offset(data, index, fstart, Val(f_dim(data)))
206+
return set_struct_linear!(
207+
root,
208+
convert(eltype(data), value),
209+
base,
210+
fstride,
211+
Val(ncomponents(data)),
212+
)
213+
end
214+
73215
# Always convert to the element type of a DataLayout when modifying its values.
74216
# Represent every single-point DataLayout view using a zero-dimensional DataF.
75217
@propagate_inbounds Base.setindex!(data::DataLayout, value, index::PointIndex) =
76218
is_invalid_linear(data, index) ? setindex!(data, value, CartesianIndices(data)[index]) :
77219
is_trivial_point(data, index) ?
78220
set_struct!(parent(data), convert(eltype(data), value)) :
79-
set_struct!(parent(data), convert(eltype(data), value), index, Val(f_dim(data)))
221+
_setidx_flat!(flat_mode(data), data, value, index)
80222
@propagate_inbounds Base.getindex(data::DataLayout, index::PointIndex) =
81223
is_invalid_linear(data, index) ? getindex(data, CartesianIndices(data)[index]) :
82224
is_trivial_point(data, index) ? get_struct(parent(data), eltype(data)) :
83-
get_struct(parent(data), eltype(data), index, Val(f_dim(data)))
225+
_getidx_flat(flat_mode(data), data, index)
84226
@propagate_inbounds Base.view(data::DataLayout, index::PointIndex) =
85227
is_invalid_linear(data, index) ? view(data, CartesianIndices(data)[index]) :
86228
is_trivial_point(data, index) ? data :

src/DataLayouts/struct_storage.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,54 @@ julia> get_struct([0 2; 0 0; 0 1; 0 0], Tuple{Int32, Int32, Int128}, 5, Val(1))
227227
return bitcast_struct(T, array, Val(Nf), index...)
228228
end
229229

230+
"""
231+
get_struct_linear(array, T, base, fstride, Val(Nf))
232+
set_struct_linear!(array, value, base, fstride, Val(Nf))
233+
234+
Flat-storage analogues of [`get_struct`](@ref) and [`set_struct!`](@ref) that
235+
address the `Nf` entries of a single value with a fully affine linear index into
236+
`array`'s storage: entry `f` lives at `base + (f - 1) * fstride`. Both `base` and
237+
`fstride` are expected to be constant-folded functions of a [`DataLayout`](@ref)'s
238+
static shape parameters (see `_flat_point_offset` in `indexing.jl`).
239+
240+
The key property is that `array[base + (f - 1) * fstride]` is a *linear* index:
241+
- for a plain `Array` it lowers to a native `arrayref` with a precomputed offset;
242+
- for a fast (`IndexLinear`) property-view `SubArray` Base lowers it to
243+
`parent[offset1 + stride1 * i]`, i.e. a single load into the root buffer with
244+
no Cartesian reindex, no `div`/`rem`, and no `SignedMultiplicativeInverse`.
245+
246+
The caller must only use these when `array` has `IndexLinear` storage; otherwise
247+
the linear index would trigger Base's expensive linear-to-Cartesian conversion.
248+
"""
249+
@inline function get_struct_linear(
250+
array,
251+
::Type{T},
252+
base,
253+
fstride,
254+
::Val{Nf},
255+
) where {T, Nf}
256+
# Bounds-check only the two endpoints of the arithmetic progression, which
257+
# bound every intermediate index; constructing a StepRange here would emit a
258+
# checked-remainder + overflow chain that dwarfs the actual read.
259+
@boundscheck (checkbounds(array, base); checkbounds(array, base + (Nf - 1) * fstride))
260+
return bitcast_struct_linear(T, array, Val(Nf), base, fstride)
261+
end
262+
263+
@inline function set_struct_linear!(
264+
array,
265+
value::T,
266+
base,
267+
fstride,
268+
::Val{Nf},
269+
) where {T, Nf}
270+
@boundscheck (checkbounds(array, base); checkbounds(array, base + (Nf - 1) * fstride))
271+
entries = bitcast_struct(NTuple{Nf, eltype(array)}, value)
272+
unrolled_foreach(enumerate(entries)) do (i, entry)
273+
@inbounds array[base + (i - 1) * fstride] = entry
274+
end
275+
return array
276+
end
277+
230278
# Indices for a view of one value's entries, chosen so the view never has more than one
231279
# dimension: Cartesian indices split into scalar components plus a range along the F axis.
232280
# A Cartesian range would instead build a much costlier view with all singleton dimensions.

0 commit comments

Comments
 (0)