Module Ir.Register_tile

The C-tile geometry of the register-tiled Tile_mma rendering (gh-ocannl-619).

The C backends render a whole-K Tile_mma tinyBLAS-style: an rm x rn grid of lanes-wide vector registers holds the accumulator across the entire k-loop — per k step, rn B-row vector loads, rm A-element splats and rm * rn fused FMAs — and the rows and columns the full rm x (rn * lanes) passes do not cover are narrower register tiles of the same form (gh-ocannl-620; coverage), never scalar code. Until gh-ocannl-619 that geometry was picked inside the renderer alone; it is now a value a schedule can carry (Schedule.optop.Tensorize's tile), the renderer honours or declines (C_syntax.try_register_tile), and the sketch seeding proposes alternatives of (alternatives), so the tuner picks by timing. This module is a leaf — below Low_level, which stores the geometry on its Tile_mma statement — so the ranking model and the fit rules live in one place both the emission and the seeding consult. Every entry point that reads both the vector width and the register count takes them as one Vector_file.t (gh-ocannl-1399), so no caller can pass a width with a count of 0 by omission.

type t = {
  1. rm : Base.int;
  2. rn : Base.int;
  3. lanes : Base.int;
}

rm accumulator rows, rn vector columns, lanes elements per vector: an rm x rn register grid covering rm rows by rn * lanes columns per pass.

val compare : t -> t -> Base.int
val equal : t -> t -> Base.bool
val sexp_of_t : t -> Base.Sexp.t
val t_of_sexp : Base.Sexp.t -> t
val to_string : t -> Base.string

"rm4 rn2 lanes8", the display form shared by decline diagnostics and family-tree labels.

val width : t -> Base.int

rn * lanes, the columns one tile pass covers.

val live_registers : t -> Base.int

rm * rn + rm + rn: the accumulator grid plus the per-k-step A splats and B rows, all at once. The register-pressure term of the model's budget and of the default tie-break -- a deliberate over-count, since no allocation needs every A splat and every B row of a k step live together; resident_floor is what one does need.

val resident_floor : t -> Base.int

rm * rn + min rm rn + 1: the fewest vector registers a pass needs to keep every operand resident -- the accumulator grid, the smaller of its two operand sets (the rn B rows reused down the rows, or the rm A splats reused across the columns), and one register streaming the other. The physical count: what rn_cap fits to the hardware's register file, and what test/operations/cc_march_census's resident C-tile claim holds against each target's registers.

val simd_lane_ladder : vector_bytes:Base.int -> elt_bytes:Base.int -> Base.int Base.list

The vector widths (in lanes) a vector_bytes-wide register file renders for elt_bytes-wide elements, widest first, halving down to the 32-byte floor (never below vector_bytes itself): 64 bytes at f32 gives 16; 8, 32 bytes gives 8, 16 bytes gives 4. Widths of fewer than two lanes are dropped. Re-exported by Backend_intf.simd_lane_ladder, where the Vectorized renderings' width choices (simd_lanes_for and its accumulating variants) build on it.

val renderable_lanes : vector_bytes:Base.int -> elt_bytes:Base.int -> Base.int Base.list

The widths (in lanes, widest first) a requested geometry may name on a vector_bytes-wide file at elt_bytes-wide elements: the file's own simd_lane_ladder, and that of every narrower file it contains -- each halving of the width down to the 8-byte floor every rendering file meets. So a geometry minted for a 16-byte file renders on a 32- or 64-byte one (gh-ocannl-1317): a seed population or a schedule tuned at the narrower width keeps its register tile on the wider machine instead of declining to the scalar fallback. Sound because a narrower vector takes no more of the file than a full one (the low part of the same register: xmm of ymm/zmm, a NEON D of its Q), so the file's count, and the budget it sets, holds for it; where the count was divided for a configured width wider than a native register it only under-counts. default and alternatives still rank only the file's own ladder: below it the renderer would trade throughput for nothing, which is a request's choice to make, never the model's.

val budget : vector_registers:Base.int -> Base.int

The live-register budget a requested geometry must fit: the live_registers of the widest default tile, rm_cap x rn_cap -- tinyBLAS's 4 x 3 on 16-register files (AVX2, SSE) and 4 x 6 on 32-register files (NEON, AVX-512), i.e. 19 and 34. Those exceed 16 and 32 because live_registers over-counts: both tiles are resident by resident_floor (16 and 29), which is where the hardware enters, through rn_cap. The budget then caps every other row count at the widest default's model pressure. A geometry over it is not wrong, only likely to spill; it is declined rather than rendered so a candidate never times a spilling kernel under a label that promised a tile.

val rn_cap : vector_registers:Base.int -> Base.int

The default model's rn ceiling: the largest rn whose rm_cap-row pass fits vector_registers by resident_floor -- 6 on 32-register files, 3 on 16, 0 when not even one column fits. Keyed on the register COUNT, which the hardware limits carry (Backend_intf.hardware_limits.simd_vector), not on the vector width: a 16-byte file is 32 registers on NEON and 16 on SSE, and keying on the width gave SSE NEON's 4 x 6, which spills there (gh-ocannl-1285). The A/B behind keeping 6 on 32 registers, where the model's live_registers reads 34 (gh-ocannl-1285, AVX-512 Zen 5 and NEON M-series): at tail-free sites (f32 n = 96, 192, 384, where 4 x 6 is the default with no tie involved) 4 x 6 won 75 of 100 serial pairs against 4 x 5 and 4 x 4 on AVX-512, 67 of 100 on NEON.

val rm_cap : Base.int

The rm ceiling — tinyBLAS's four accumulator rows, which default takes wherever the row extent affords them. Exposed so that a geometry request naming only rn (bin/narrow_gebp_bench and bin/schedule_bench's --rn=) derives the rows the renderer would have chosen rather than restating the constant.

type coverage = {
  1. m_full : Base.int;
  2. n_full : Base.int;
  3. tail_widths : Base.int Base.list;
}

How a geometry covers an m x n site (gh-ocannl-620): m_full rows by n_full columns of full rm x (rn * lanes) passes; the n - n_full leftover columns as a column tail of tail_widths vector columns of lanes lanes each — every entry is lanes except a last, PARTIAL one whose entry is its valid lane count; and the m - m_full leftover rows as a band of that many rows over the same columns. The renderer emits exactly this decomposition (a partial column loads zeros past its width and stores only its width), so nothing is peeled to scalar code and every element's k-chain is the same fused serial chain.

val coverage : m:Base.int -> n:Base.int -> t -> coverage
val default : file:Vector_file.t -> elt_bytes:Base.int -> m:Base.int -> n:Base.int -> t Base.option

The renderer's own choice for an m x n site when the schedule carries no geometry: rm = min 4 m, and (lanes, rn) ranked over the widths the file renders (the ladder, narrowed to those n can fill) and rn <= rn_cap by a ranking model in vector-issue slots per unit of m*k: one fused FMA per vector column plus the B loads (1/rm per FMA) and the A splats (1/rn) — for the full passes at rn, and for the column tail at its own, smaller column count (the tail is a narrower tile, gh-ocannl-620; a partial last vector is a whole issue). No fitted constant: until gh-ocannl-620 the tail was a scalar peel priced at a measured 10 lane-slots per column (gh-ocannl-575), which made the width a divisibility question — now it is a reuse question, and the model only has to RANK. Summed over the site the price is ceil(n / lanes) * (1 + 1/rm) plus ceil(n / (rn * lanes)) A-splat passes, which the ranking computes as an exact integer (scaled by rm) so that equal prices reach the tie-break rather than being decided by float rounding (gh-ocannl-947). At one width the columns' term is the same for every rn, so the pick is the fewest passes whatever the row count. Ties go to the wider vector, then to a tail-free tile (one tile body rather than two), then to the smaller tile: two tail-bearing tiles of equal price at one width have the same passes, A splats and C-tile traffic (gh-ocannl-1099), and the smaller one holds fewer live_registers (gh-ocannl-1180). None when even the narrowest width exceeds n (or m, n < 1).

val check : file:Vector_file.t -> elt_bytes:Base.int -> m:Base.int -> n:Base.int -> t -> (Base.unit, Base.string) Base.Result.t

Whether the renderer can honour t on an m x n site: lanes is one of the renderable_lanes and n fills it, the register file holds some rm_cap-row tile at all, rm <= m, rn * lanes <= n, and live_registers within budget. The Error names the violated rule — it becomes the decline diagnostic.

val alternatives : file:Vector_file.t -> elt_bytes:Base.int -> m:Base.int -> n:Base.int -> t Base.list

The geometries the sketch seeding proposes beside the renderer's default, for the tuner to time: at the widest width n fills and the default's rm, the largest tail-free rn >= 2 (the width divides n exactly, so the site is one tile body: what a tail-bearing default's second, lower-reuse tile is traded against — the smaller tail-free widths are dominated on the model's own terms, equal issues at less reuse, so they are not seeded) plus the register-budget cap when its column tail is at most one vector (the most A-reuse the file affords — the register-pressure corner gh-ocannl-614 found the model cannot see — but not on sites where the tail is a fat second tile the model already prices), plus, where default broke a price tie between two tail-bearing tiles, the largest tile it tied (the model prices no difference there, so the larger-tile side of the tie is timed; gh-ocannl-1180), minus the default itself and anything check would decline. Deliberately small: at most two alternatives per site at four rows (where the tied tile is the budget cap), three below, one on the common shapes, none where the width divides the extent.

val rm_twin : file:Vector_file.t -> elt_bytes:Base.int -> m:Base.int -> n:Base.int -> t Base.option

The two-row twin of default (gh-ocannl-947), which the sketch seeding proposes beside alternatives only under the autotune_register_tile_rm_twin config key (off by default): on a site whose default takes rm_cap rows, rm = 2 at the default's lanes with the largest rn the live-register budget admits beside two rows (capped by n / lanes) -- the least register pressure and the most A-reuse a two-row tile has. The ranking model always prefers the four-row default (at rm = 2 every B vector is reloaded twice as often), so whether the twin ever wins is a timing question, and seeding it on every leaf would grow the candidate count for a shape the tuner may always reject. It never repeats the default or one of the alternatives, which all take the default's four rows. None where the default has fewer rows or where check would decline the twin.