Skip to main content

et_kernel/
tensor.rs

1//! Tensor-extension intrinsics for the ET-SoC-1 Minion core.
2//!
3//! All tensor instructions on the ET-SoC-1 are encoded as standard RISC-V
4//! `csrrw xd, <csr>, xs` writes (see PRM Chapter 9). No custom opcode or
5//! target-feature extension is required: `riscv64imac` suffices because the
6//! operand registers are ordinary integer GPRs (the source value `xs` is an
7//! integer register; the FP register file is accessed implicitly by the
8//! tensor co-processor hardware, not by the instruction encoding).
9//!
10//! # Concurrency model
11//!
12//! The tensor co-processor operates independently of the RISC-V hart's
13//! integer pipeline. Issuing a tensor instruction initiates an asynchronous
14//! operation; the hart must call [`tensor_wait`] with the appropriate
15//! [`TensorEvent`] before reading results or reusing the scratchpad. The
16//! ordering guarantees are:
17//!
18//! - `TensorWait(Load0)` before `tensor_fma32` / `tensor_fma16a32` /
19//!   `tensor_ima8a32`: scratchpad A (and B when TENB=0) is populated.
20//! - `TensorWait(Fma)` before `tensor_store` / `tensor_store_from_scp`:
21//!   FP register file (or TenC for IMA8A32 with DST=0) holds final C.
22//! - `TensorWait(Store)` drains only tensor store DMA; prefer it over a full
23//!   `fence rw, rw` when only tensor-store ordering is required.
24//! - `TensorWait(LoadL2_0)` or `TensorWait(LoadL2_1)` after
25//!   [`tensor_load_l2`]: the shire L2 prefetch has completed.
26//! - `TensorWait(CacheOp)` after `cache_writeback` / `cache_invalidate` /
27//!   `cache_flush`: all L1 cache management operations have completed.
28//! - `fence rw, rw` (via [`crate::fence`]) after the final store: writes are
29//!   visible to other Minions and the DMA engine before the kernel returns.
30//!
31//! # Scratchpad layout
32//!
33//! Each Minion has a private 48-line L1 scratchpad (3 072 bytes). Only the
34//! primary hart of the Minion (hart 0, i.e. `mhartid & 1 == 0`) should issue
35//! tensor load/store/FMA instructions; the companion hart (hart 1) must not
36//! touch the same scratchpad lines concurrently.
37
38use core::arch::asm;
39
40// ---------------------------------------------------------------------------
41// CSR addresses (PRM Chapter 9, Table 9-1)
42// ---------------------------------------------------------------------------
43
44/// TensorFMA CSR (`tensor_fma`): selects the FMA variant via xs bits 3:1.
45/// (PRM Table 9-7: TensorFMA32 = 3:1 000, TensorFMA16A32 = 001, ...)
46pub const CSR_TENSOR_FMA:   u16 = 0x801;
47/// TensorWait CSR (`tensor_wait`): stalls the hart until the requested event.
48pub const CSR_TENSOR_WAIT:  u16 = 0x830;
49/// TensorError CSR (`tensor_error`): latched error flags from the co-processor.
50/// (PRM Table 9-1: 0x808, not 0x831)
51pub const CSR_TENSOR_ERROR: u16 = 0x808;
52/// TensorMask CSR (`tensor_mask`): per-row enable bits for the A tile.
53/// (PRM Table 9-1: 0x805, not 0x832)
54pub const CSR_TENSOR_MASK:  u16 = 0x805;
55/// TensorStore CSR (`tensor_store`): store from FP registers (bit 48 = 0) or
56/// from the L1 scratchpad (bit 48 = 1 = TensorStoreFromScp) to memory.
57/// (PRM Table 9-7: 0x87F, not 0x83E)
58pub const CSR_TENSOR_STORE: u16 = 0x87F;
59/// TensorLoad / TensorLoadB CSR (`tensor_load`): load from memory to the L1
60/// scratchpad (xs bit 52 = 0) or to the TenB register file (bit 52 = 1).
61pub const CSR_TENSOR_LOAD:    u16 = 0x83F;
62/// TensorLoadL2Scp CSR: loads rows from memory to the shire L2 cache without
63/// consuming any L1 scratchpad lines. Useful for prefetching A strips while
64/// the current k-loop tile executes, so the subsequent `tensor_load` (L1 fill)
65/// completes from L2 rather than DRAM.
66pub const CSR_TENSOR_LOAD_L2: u16 = 0x85F;
67/// TensorReduce CSR (`tensor_reduce`): hart-to-hart register-file exchange.
68/// xs bits 1:0 select the variant: TensorSend=00, TensorRecv=01,
69/// TensorBroadcast=10, TensorReduce=11. (PRM Table 9-7: 0x800)
70pub const CSR_TENSOR_REDUCE:  u16 = 0x800;
71
72// ---------------------------------------------------------------------------
73// TensorWait event codes (PRM Table 9-2, xs bits 3:0)
74// ---------------------------------------------------------------------------
75
76/// Tensor co-processor synchronisation events for [`tensor_wait`].
77///
78/// The four-bit EVENT field in the TensorWait `xs` register selects which
79/// outstanding operation the hart waits for before the instruction retires.
80/// (PRM Table 9-2.)
81///
82/// This enum is `#[non_exhaustive]`: match arms outside this crate must
83/// include a wildcard arm.
84#[non_exhaustive]
85#[repr(u8)]
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum TensorEvent {
88    /// Completion of all TensorLoad operations issued with ID = 0 (event 0).
89    Load0     = 0,
90    /// Completion of all TensorLoad operations issued with ID = 1 (event 1).
91    Load1     = 1,
92    /// Completion of a TensorLoadL2Scp issued with ID = 0 (event 2).
93    /// Use this after [`tensor_load_l2`] with `id = false`.
94    /// Not the same as `CacheOp` (event 6).
95    LoadL2_0  = 2,
96    /// Completion of a TensorLoadL2Scp issued with ID = 1 (event 3).
97    /// Use this after [`tensor_load_l2`] with `id = true`.
98    LoadL2_1  = 3,
99    /// Completion of L2/L3 prefetch operations with ID = 0 (event 4).
100    Prefetch0 = 4,
101    /// Completion of L2/L3 prefetch operations with ID = 1 (event 5).
102    Prefetch1 = 5,
103    /// Completion of all preceding L1 cache management operations: EvictVA
104    /// and FlushVA (event 6). Required after [`cache::cache_writeback`],
105    /// [`cache::cache_invalidate`], or [`cache::cache_flush`] before issuing
106    /// memory accesses to the affected cache lines.
107    ///
108    /// **Note:** TensorLoadL2Scp requires `LoadL2_0`/`LoadL2_1` (events 2/3),
109    /// not this event. L2/L3 prefetch requires `Prefetch0`/`Prefetch1` (events
110    /// 4/5). The cache op functions already issue this wait internally; use this
111    /// variant directly only when batching cache ops and deferring the wait.
112    CacheOp   = 6,
113    /// Completion of all preceding TensorFMA operations (event 7). The FP
114    /// register file holds the final accumulated C tile and may be read or
115    /// stored.
116    Fma       = 7,
117    /// Completion of all preceding TensorStore DMA transfers (event 8).
118    /// Drains only the tensor store DMA; prefer this over a full
119    /// `fence rw, rw` when only tensor-store ordering is required.
120    Store     = 8,
121    /// Completion of all preceding TensorSend/TensorRecv operations (event 9).
122    /// Required after [`tensor_recv`] before reading the FP registers updated
123    /// by the receive.
124    TensorReduce = 9,
125    /// Completion of all preceding TensorQuant operations (event 10).
126    TensorQuant  = 10,
127}
128
129// ---------------------------------------------------------------------------
130// TensorError (PRM Table 9-3)
131// ---------------------------------------------------------------------------
132
133/// Tensor co-processor error status, returned by [`check_tensor_error`].
134///
135/// The raw value is the 64-bit content of the `tensor_error` CSR (0x808).
136/// Named bit accessors will be added once PRM Table 9-3 bit positions are
137/// confirmed on hardware. Use [`raw`](TensorError::raw) to inspect the value
138/// directly in the interim.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub struct TensorError(u64);
141
142impl TensorError {
143    /// Returns the raw CSR value as read from `tensor_error` (CSR 0x808).
144    #[inline]
145    pub fn raw(self) -> u64 {
146        self.0
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Public intrinsic functions
152// ---------------------------------------------------------------------------
153
154/// Stall the hart until the specified tensor co-processor event fires.
155///
156/// This must be called between dependent tensor operations to enforce ordering
157/// -- the co-processor and the hart pipeline are otherwise decoupled.
158#[inline(always)]
159pub fn tensor_wait(event: TensorEvent) {
160    let xs: u64 = event as u64;
161    // SAFETY: csrrw to a U-mode-accessible tensor CSR with no memory effects
162    // from the hart's perspective; the co-processor drains its pipeline.
163    unsafe {
164        asm!(
165            concat!("csrrw x0, ", stringify!(0x830), ", {xs}"),
166            xs = in(reg) xs,
167            options(nomem, nostack, preserves_flags),
168        );
169    }
170}
171
172/// Read the tensor co-processor error status register.
173///
174/// Returns 0 when no error has occurred since the last reset. A non-zero
175/// value encodes the error class in bits defined by PRM Table 9-3. Call
176/// after `tensor_wait` to check for co-processor faults. Prefer
177/// [`check_tensor_error`] to obtain a typed result.
178#[must_use = "tensor_error() returns the co-processor fault status; \
179              a non-zero value indicates a hardware error that must be handled"]
180#[inline(always)]
181pub fn tensor_error() -> u64 {
182    let v: u64;
183    // SAFETY: csrrs with rs1 = x0 reads without side effect.
184    unsafe {
185        asm!(
186            concat!("csrrs {v}, ", stringify!(0x808), ", x0"),
187            v = out(reg) v,
188            options(nomem, nostack, preserves_flags),
189        );
190    }
191    v
192}
193
194/// Check the tensor co-processor error register and return a typed result.
195///
196/// Returns `Ok(())` when no fault has been latched. Returns `Err(TensorError)`
197/// containing the raw CSR value otherwise. Call after `tensor_wait` to verify
198/// that the preceding tensor operation completed without fault. Named bit
199/// accessors on [`TensorError`] will be added once PRM Table 9-3 bit positions
200/// are confirmed on hardware.
201///
202/// # Example
203/// ```no_run
204/// # use et_kernel::tensor::{TensorEvent, tensor_wait, check_tensor_error};
205/// # unsafe {
206/// tensor_wait(TensorEvent::Fma);
207/// check_tensor_error().expect("TensorFMA fault");
208/// # }
209/// ```
210#[inline(always)]
211pub fn check_tensor_error() -> Result<(), TensorError> {
212    let v = tensor_error();
213    if v == 0 { Ok(()) } else { Err(TensorError(v)) }
214}
215
216/// Initiate an asynchronous TensorLoadL2Scp from memory into the shire L2 cache.
217///
218/// Identical to [`tensor_load`] in xs encoding and x31 convention, but targets
219/// CSR `0x85F` (TensorLoadL2Scp) rather than `0x83F`. The rows are loaded into
220/// the shire L2 without consuming any L1 scratchpad lines. Use this to prefetch
221/// A strips while the current k-loop FMA executes; the subsequent
222/// [`tensor_load`] for the same address will then complete from L2 rather than
223/// DRAM, removing A-DMA latency from the FMA critical path.
224///
225/// # Parameters
226/// - `addr`: 64-byte aligned virtual address of the first row in memory.
227/// - `start`: L2 target line index.
228/// - `rows`: rows to load minus one (0..=15).
229/// - `id`: selects the wait event (false = `LoadL2_0`, true = `LoadL2_1`).
230///   Use `LoadL2_1` when a [`tensor_load`] with `id: false` is also in flight.
231/// - `stride`: row stride in bytes (64-byte aligned); placed in x31.
232///
233/// Call `tensor_wait(TensorEvent::LoadL2_0)` (or `LoadL2_1` if `id = true`)
234/// before the scratchpad fill from the same address. Do not use `CacheOp`
235/// (event 6) -- TensorLoadL2Scp requires events 2/3 per PRM Table 9-2.
236///
237/// # Safety
238/// Same constraints as [`tensor_load`]: `addr` must be aligned and within
239/// device memory; must be called from the primary hart.
240#[inline(always)]
241pub unsafe fn tensor_load_l2(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
242    debug_assert!(addr.is_multiple_of(64), "tensor_load_l2: addr must be 64-byte aligned");
243    // xs layout is identical to TensorLoad; only the CSR address differs.
244    let xs: u64 = ((start as u64 & 0x3F) << 53)
245               |  (addr as u64)
246               |  (rows as u64 & 0xF);
247    unsafe {
248        asm!(
249            "mv t6, {stride}",
250            concat!("csrrw x0, ", stringify!(0x85F), ", {xs}"),
251            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
252            xs     = in(reg) xs,
253            out("t6") _,
254            options(nostack),
255        );
256    }
257}
258
259/// Write the per-row enable mask for the next TensorFMA.
260///
261/// Bit `i` in `mask` enables row `i` of the A tile. Setting bit `i = 0`
262/// suppresses the update to C row `i` (useful for partial M tiles when the
263/// mask register is more convenient than setting AROWS). For most uses,
264/// leave the mask at its reset value of all-ones and control the tile size
265/// via the AROWS field in [`tensor_fma32`].
266#[inline(always)]
267pub fn set_tensor_mask(mask: u16) {
268    let xs: u64 = mask as u64;
269    unsafe {
270        asm!(
271            concat!("csrrw x0, ", stringify!(0x805), ", {xs}"),
272            xs = in(reg) xs,
273            options(nomem, nostack, preserves_flags),
274        );
275    }
276}
277
278/// Initiate an asynchronous TensorLoad from memory into the L1 scratchpad.
279///
280/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into
281/// L1 scratchpad lines `start` through `start + rows`. Row `i` is read from
282/// address `addr + i * stride`. The operation is asynchronous: call
283/// `tensor_wait(TensorEvent::Load0)` (or `Load1` if `id = true`) before
284/// reading the scratchpad in a subsequent [`tensor_fma32`].
285///
286/// # Parameters
287/// - `addr`: 64-byte aligned virtual address of the first row in memory.
288/// - `start`: L1 scratchpad starting line index (0..=47).
289/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
290///   Loads `rows + 1` cache lines.
291/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
292/// - `stride`: row stride in bytes (64-byte aligned); placed in x31 by this
293///   function immediately before the CSRRW instruction.
294///
295/// # Safety
296/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` valid,
297///   readable bytes of device memory.
298/// - Must be called from the primary hart of the Minion (mhartid & 1 == 0).
299#[inline(always)]
300pub unsafe fn tensor_load(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
301    debug_assert!(addr.is_multiple_of(64), "tensor_load: addr must be 64-byte aligned");
302    // xs bit layout (PRM Table 9-5):
303    //   63: MSK=0, 62: COOP=0, 61:59=000 (TensorLoad variant),
304    //   58:53=START (6-bit scratchpad line index),
305    //   52=0 (TensorLoad, not TensorLoadB),
306    //   51:48=0 (reserved), 47:6=ADDR>>6 (addr is 64B-aligned so bits 5:0 = 0),
307    //   5:4=0 (reserved), 3:0=ROWS.
308    let xs: u64 = ((start as u64 & 0x3F) << 53)
309               |  (addr as u64)           // bits 47:6; addr is 64B-aligned so addr & !63 == addr
310               |  (rows as u64 & 0xF);
311    // x31 (t6) carries the row stride; the hardware reads it implicitly.
312    unsafe {
313        asm!(
314            "mv t6, {stride}",
315            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
316            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
317            xs     = in(reg) xs,
318            out("t6") _,
319            options(nostack),
320        );
321    }
322}
323
324/// Initiate an asynchronous TensorLoadB from memory into the TenB register file.
325///
326/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into the
327/// dedicated TenB buffer. This forward-pairs with the next [`tensor_fma32`]
328/// call that uses `tenb = true`; the FMA waits internally for the load to
329/// complete, so no explicit `tensor_wait` is needed between LoadB and FMA.
330///
331/// # Parameters
332/// - `addr`: 64-byte aligned virtual address of the first B row in memory.
333/// - `rows`: B rows to load minus one (ACOLS of the subsequent FMA, 0..=15).
334/// - `coop`: set for cooperative multi-hart loading (advanced; leave false).
335/// - `stride`: row stride of B in bytes (64-byte aligned); placed in x31.
336/// - `id`: load event identifier placed in bit 0 of x31 (false = `Load0`,
337///   true = `Load1`). Use `Load1` when a `tensor_load` with `id: false` is
338///   also in flight, so that `tensor_wait(Load0)` waits only for the A tile
339///   and not for the B DMA (which forward-pairs with the FMA anyway).
340///
341/// # Note: TenB path has no hardware interleave variant
342///
343/// The TenB register-file path (xs bit 52 = 1) does not support hardware
344/// interleaving of consecutive fp16 rows. B must be pre-packed host-side into
345/// the 2-row-interleaved layout that FMA16A32 expects before upload.
346/// `TensorLoadInterleave16` (xs bits 61:59 = 010, xs bit 52 = 0) interleaves
347/// from plain row-major fp16 in DRAM into the L1 scratchpad, but it targets
348/// the scratchpad path (bit 52 = 0), not the TenB register file; there is no
349/// interleave variant for the TenB path.
350///
351/// # Safety
352/// Same alignment and primary-hart constraints as [`tensor_load`].
353#[inline(always)]
354pub unsafe fn tensor_load_b(addr: usize, rows: u8, coop: bool, stride: u64, id: bool) {
355    debug_assert!(addr.is_multiple_of(64), "tensor_load_b: addr must be 64-byte aligned");
356    // xs bit layout (PRM Table 9-6):
357    //   63: MSK=0, 62: COOP, 61:53=0 (reserved),
358    //   52=1 (TensorLoadB distinguisher),
359    //   51:48=0 (reserved), 47:6=ADDR>>6, 5:4=0, 3:0=ROWS.
360    // x31 bit 0 = ID (identical mechanism to TensorLoad; PRM Chapter 9).
361    let xs: u64 = ((coop as u64)  << 62)
362               |  (1_u64          << 52)
363               |  (addr as u64)           // 64B-aligned: bits 47:6 correct
364               |  (rows as u64 & 0xF);
365    unsafe {
366        asm!(
367            "mv t6, {stride}",
368            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
369            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
370            xs     = in(reg) xs,
371            out("t6") _,
372            options(nostack),
373        );
374    }
375}
376
377/// Build the xs value for a TensorFMA32 instruction.
378///
379/// The FMA computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when
380/// `mul_only = true`), accumulating into the FP register file.
381///
382/// # Parameters
383/// - `bcols`:    B column groups minus one (BCOLS field, 0..=3; output columns
384///   = 4*(bcols+1), e.g. 3 -> 16 columns).
385/// - `arows`:    A tile rows minus one (AROWS field, 0..=15).
386/// - `acols`:    A tile columns minus one (ACOLS field, 0..=15); also the
387///   number of B rows loaded by the preceding [`tensor_load_b`].
388/// - `aoffset`:  byte offset within each scratchpad line where A row data
389///   begins, in 4-byte units (AOFFSET, 0..=15). Use 0 when A columns start
390///   at the beginning of a cache line.
391/// - `tenb`:     `true` to read B from the TenB register file (filled by the
392///   preceding [`tensor_load_b`]); `false` to read from the L1 scratchpad
393///   at `bstart`.
394/// - `bstart`:   scratchpad line index of B (ignored when `tenb = true`).
395/// - `astart`:   scratchpad line index of A (ASTART field, 0..=47).
396/// - `mul_only`: `true` for C = A*B (ignore existing FP register values);
397///   `false` for C += A*B (accumulate into current FP registers).
398/// - `use_mask`: apply the tensor_mask row-enable register.
399#[must_use = "the returned xs value must be passed to tensor_fma32; discarding it issues no instruction"]
400#[allow(clippy::too_many_arguments)]
401#[inline]
402pub fn fma32_xs(
403    bcols:    u8,
404    arows:    u8,
405    acols:    u8,
406    aoffset:  u8,
407    tenb:     bool,
408    bstart:   u8,
409    astart:   u8,
410    mul_only: bool,
411    use_mask: bool,
412) -> u64 {
413    // xs bit layout (PRM Table 9-4):
414    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
415    //   50:47: ACOLS, 46:43: AOFFSET, 42:21: reserved (0), 20: TENB,
416    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
417    //   9:4: ASTART, 3:1: 000 (FMA32 TensorType), 0: MUL.
418    ((use_mask as u64)       << 63)
419  | ((bcols   as u64 & 0x3)  << 55)
420  | ((arows   as u64 & 0xF)  << 51)
421  | ((acols   as u64 & 0xF)  << 47)
422  | ((aoffset as u64 & 0xF)  << 43)
423  | ((tenb    as u64)        << 20)
424  | ((bstart  as u64 & 0x3F) << 12)
425  | ((astart  as u64 & 0x3F) <<  4)
426  // bits 3:1 = 000 (FMA32 TensorType selector)
427  | (mul_only as u64)
428}
429
430/// Initiate an asynchronous TensorFMA32.
431///
432/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma32_xs`]. The
433/// operation is asynchronous: call `tensor_wait(TensorEvent::Fma)` before
434/// reading the FP register file or issuing a subsequent [`tensor_store`].
435///
436/// # Safety
437/// - The L1 scratchpad must be fully populated (TensorLoad with subsequent
438///   `tensor_wait(Load0)`) before this call when `tenb = false`, or
439///   equivalently [`tensor_load_b`] must have been issued before this call
440///   for the TenB path.
441/// - Must be called from the primary hart of the Minion.
442#[inline(always)]
443pub unsafe fn tensor_fma32(xs: u64) {
444    unsafe {
445        asm!(
446            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
447            xs = in(reg) xs,
448            options(nostack),
449        );
450    }
451}
452
453/// Build the xs value for a TensorFMA16A32 instruction.
454///
455/// Computes C += A * B with fp16 inputs and fp32 accumulation. The hardware
456/// processes two K-columns per clock in a fused 3-way addition that is not
457/// IEEE754-equivalent to two separate adds: the internal partial sum is
458/// unrounded, and the final result is rounded toward zero (RTZ), not to
459/// nearest. This introduces a systematic truncation bias. Measured RMS
460/// relative error is approximately 2.6e-4 against an fp32 reference, flat
461/// between K=2048 and K=4096; input-rounding dominates the RTZ bias at those
462/// depths. The xs bit layout is identical to [`fma32_xs`] except bits 3:1 =
463/// `001` (FMA16A32 TensorType selector).
464///
465/// # Tile geometry -- ACOLS differs from FMA32
466///
467/// The ACOLS field contracts a different number of K elements than in
468/// [`fma32_xs`]:
469///
470/// - `acols = n` means K = 2*(n+1) fp16 pairs (two K-columns per step).
471///   For example, `acols=0` -> K=2, `acols=15` -> K=32.
472/// - Each A row in the L1 scratchpad occupies `(acols+1)*4` bytes,
473///   holding `(acols+1)*2` fp16 values (two per group of ACOLS+1 groups).
474/// - The paired [`tensor_load_b`] must be issued with `rows = acols`; the
475///   hardware fires `tensor_error[6]` if `LoadB.ROWS != ACOLS`.
476///
477/// # Parameters
478/// (same names as [`fma32_xs`]; `tenb = true` selects the TenB register file
479/// for B.)
480#[must_use = "the returned xs value must be passed to tensor_fma16a32; \
481              discarding it issues no instruction"]
482#[allow(clippy::too_many_arguments)]
483#[inline]
484pub fn fma16a32_xs(
485    bcols:    u8,
486    arows:    u8,
487    acols:    u8,
488    aoffset:  u8,
489    tenb:     bool,
490    bstart:   u8,
491    astart:   u8,
492    mul_only: bool,
493    use_mask: bool,
494) -> u64 {
495    // xs bit layout (PRM Table 9-4, TensorFMA16A32 variant):
496    //   identical to TensorFMA32 (fma32_xs) except bits 3:1 = 001.
497    ((use_mask as u64)       << 63)
498  | ((bcols   as u64 & 0x3)  << 55)
499  | ((arows   as u64 & 0xF)  << 51)
500  | ((acols   as u64 & 0xF)  << 47)
501  | ((aoffset as u64 & 0xF)  << 43)
502  | ((tenb    as u64)        << 20)
503  | ((bstart  as u64 & 0x3F) << 12)
504  | ((astart  as u64 & 0x3F) <<  4)
505  | (1_u64                    <<  1)  // bits 3:1 = 001 (FMA16A32 TensorType)
506  | (mul_only as u64)
507}
508
509/// Initiate an asynchronous TensorFMA16A32.
510///
511/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma16a32_xs`].
512/// The hardware selects the FMA16A32 path via bits 3:1 = `001` in xs.
513/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
514///
515/// # Safety
516/// Same constraints as [`tensor_fma32`].
517#[inline(always)]
518pub unsafe fn tensor_fma16a32(xs: u64) {
519    unsafe {
520        asm!(
521            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
522            xs = in(reg) xs,
523            options(nostack),
524        );
525    }
526}
527
528/// Build the xs value for a TensorIMA8A32 instruction.
529///
530/// Computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when `mul_only = true`),
531/// where A and B hold 8-bit integer elements and C accumulates as 32-bit signed
532/// integers. The A matrix is `(AROWS+1) x (ACOLS+1)*4` int8 elements; the B
533/// matrix is `(ACOLS+1)*4 x (BCOLS+1)*16` int8 elements (interleaved 4 columns
534/// at a time); the output is `(AROWS+1) x (BCOLS+1)*4` int32 values.
535///
536/// # Parameters
537/// - `bcols`:      B column groups minus one (BCOLS, 0..=3; output columns = 4*(bcols+1)).
538/// - `arows`:      A tile rows minus one (AROWS, 0..=15).
539/// - `acols`:      A tile column groups minus one (ACOLS, 0..=15). Each group
540///   contains 4 int8 K-elements, so `acols = n` contracts K = 4*(n+1) rows
541///   (e.g. `acols=0` -> K=4, `acols=15` -> K=64). Each A row in the L1
542///   scratchpad occupies `(acols+1)*4` bytes.
543/// - `aoffset`:    Byte offset within each scratchpad line for A data, in 4-byte units
544///   (AOFFSET, 0..=15).
545/// - `b_in_mem`:   `true` if B is transferred via the memory DMA path; `false` for L1
546///   scratchpad. (TENB = 1 means memory for IMA8A32, unlike FMA where TENB=1 is TenB
547///   register file.)
548/// - `bstart`:     Starting scratchpad line for B; ignored when `b_in_mem = true`.
549/// - `astart`:     Starting scratchpad line for A (ASTART, 0..=47).
550/// - `dst_fp`:     `true` to write the int32 result to the FP register file;
551///   `false` to write to the TenC register file. (DST, xs bit 23)
552/// - `b_unsigned`: `true` if B elements are unsigned; `false` for signed.
553/// - `a_unsigned`: `true` if A elements are unsigned; `false` for signed.
554/// - `mul_only`:   `true` for C = A*B; `false` for C += A*B.
555/// - `use_mask`:   Apply the tensor_mask row-enable register.
556#[must_use = "the returned xs value must be passed to tensor_ima8a32; \
557              discarding it issues no instruction"]
558#[allow(clippy::too_many_arguments)]
559#[inline]
560pub fn ima8a32_xs(
561    bcols:      u8,
562    arows:      u8,
563    acols:      u8,
564    aoffset:    u8,
565    b_in_mem:   bool,
566    bstart:     u8,
567    astart:     u8,
568    dst_fp:     bool,
569    b_unsigned: bool,
570    a_unsigned: bool,
571    mul_only:   bool,
572    use_mask:   bool,
573) -> u64 {
574    // xs bit layout (PRM Table 9-4, TensorIMA8A32 variant):
575    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
576    //   50:47: ACOLS, 46:43: AOFFSET, 42:24: reserved (0),
577    //   23: DST (0=TenC, 1=FP registers), 22: UB, 21: UA,
578    //   20: TENB (0=L1 scratchpad, 1=memory path for IMA8A32),
579    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
580    //   9:4: ASTART, 3:1: 011 (IMA8A32 TensorType), 0: MUL.
581    ((use_mask   as u64)        << 63)
582  | ((bcols      as u64 & 0x3)  << 55)
583  | ((arows      as u64 & 0xF)  << 51)
584  | ((acols      as u64 & 0xF)  << 47)
585  | ((aoffset    as u64 & 0xF)  << 43)
586  | ((dst_fp     as u64)        << 23)
587  | ((b_unsigned as u64)        << 22)
588  | ((a_unsigned as u64)        << 21)
589  | ((b_in_mem   as u64)        << 20)
590  | ((bstart     as u64 & 0x3F) << 12)
591  | ((astart     as u64 & 0x3F) <<  4)
592  | (3_u64                       <<  1)  // bits 3:1 = 011 (IMA8A32 TensorType)
593  | (mul_only    as u64)
594}
595
596/// Initiate an asynchronous TensorIMA8A32.
597///
598/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`ima8a32_xs`].
599/// The hardware selects the integer-GEMM path via bits 3:1 = `011` in xs.
600/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
601///
602/// # Safety
603/// Same constraints as [`tensor_fma32`].
604#[inline(always)]
605pub unsafe fn tensor_ima8a32(xs: u64) {
606    unsafe {
607        asm!(
608            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
609            xs = in(reg) xs,
610            options(nostack),
611        );
612    }
613}
614
615/// Initiate an asynchronous TensorStoreFromScp to memory from the L1 scratchpad.
616///
617/// Stores `rows + 1` 64-byte scratchpad lines to memory, bypassing the L1 data
618/// cache and L2 cache. Consecutive scratchpad lines are spaced `step` lines apart
619/// (so `step = 1` stores consecutive lines); consecutive destination rows are
620/// spaced `stride` bytes apart.
621///
622/// # Parameters
623/// - `addr`:   64-byte aligned virtual address of the first destination row.
624/// - `rows`:   Number of rows to store minus one (ROWS, 0..=15).
625/// - `start`:  Starting L1 scratchpad cache line (0..=47).
626/// - `step`:   Scratchpad line stride (1..=4); encoded as STEP = step - 1.
627/// - `stride`: Destination row stride in bytes; placed in x31. Low 6 bits are
628///   ignored by the hardware (rows are 64-byte aligned in memory).
629///
630/// # Safety
631/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` bytes of
632///   writable device memory.
633/// - Must be called from the primary hart of the Minion.
634#[inline(always)]
635pub unsafe fn tensor_store_from_scp(addr: usize, rows: u8, start: u8, step: u8, stride: u64) {
636    debug_assert!(addr.is_multiple_of(64), "tensor_store_from_scp: addr must be 64-byte aligned");
637    // xs bit layout (PRM Table 9-7, TensorStoreFromScp):
638    //   63:62: STEP (step-1; scratchpad line stride), 61:56: START (first
639    //   scratchpad line), 55: reserved (0), 54:51: ROWS (rows-1), 50:49:
640    //   reserved (0), 48: 1 (TensorStoreFromScp discriminator, bit 48=0 is
641    //   TensorStore), 47:6: ADDR (virtual address with 6 low-order bits
642    //   omitted; addr is 64B-aligned), 5:0: reserved (0).
643    // x31 carries the destination row stride; low 6 bits are ignored by hardware.
644    let xs: u64 = (((step as u64).saturating_sub(1) & 0x3) << 62)
645               |  ((start as u64 & 0x3F) << 56)
646               |  ((rows  as u64 & 0xF)  << 51)
647               |  (1_u64                 << 48)         // source = L1 scratchpad
648               |  (addr as u64 & 0x0000_FFFF_FFFF_FFC0_usize as u64);  // ADDR[47:6]
649    unsafe {
650        asm!(
651            "mv t6, {stride}",
652            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
653            stride = in(reg) stride,
654            xs     = in(reg) xs,
655            out("t6") _,
656            options(nostack),
657        );
658    }
659}
660
661/// Reduction function selector for [`tensor_recv`].
662///
663/// Specifies how the received values are combined with the values already held
664/// in the destination FP registers. (PRM Table 9-8, FUNCT field.)
665#[repr(u8)]
666#[derive(Clone, Copy, Debug, PartialEq, Eq)]
667pub enum ReduceFunct {
668    /// C[i] = C[i] + src[i]  (fp32 addition)
669    Fadd  = 0,
670    /// C[i] = fmax(C[i], src[i])
671    Fmax  = 2,
672    /// C[i] = fmin(C[i], src[i])
673    Fmin  = 3,
674    /// C[i] = C[i] + src[i]  (integer addition on bit pattern)
675    Add   = 4,
676    /// C[i] = max(C[i], src[i])  (signed 32-bit integer comparison)
677    Max   = 6,
678    /// C[i] = min(C[i], src[i])  (signed 32-bit integer comparison)
679    Min   = 7,
680    /// C[i] = src[i]             (unconditional move)
681    Move  = 8,
682}
683
684/// Initiate an asynchronous TensorSend.
685///
686/// Pushes `count` consecutive FP registers starting at `freg` from this hart
687/// to hart 0 of the Minion identified by `target`. The partner hart must issue
688/// a matching [`tensor_recv`]. This is the low-level primitive for hart-to-hart
689/// reduction without software memory traffic.
690///
691/// # Parameters
692/// - `freg`:   Starting FP register index (0..=31).
693/// - `count`:  Number of FP registers to send (COUNT field, 0..=127).
694/// - `target`: Destination Minion ID (TARGET field, bits 15:3 of xs).
695///
696/// # Safety
697/// - The partner hart must call [`tensor_recv`] with the matching `source` and
698///   `count` before the send retires.
699/// - Must be called from the primary hart of the Minion.
700#[inline(always)]
701pub unsafe fn tensor_send(freg: u8, count: u8, target: u16) {
702    // xs bit layout (PRM Table 9-8, TensorSend):
703    //   63:62: reserved (0), 61:57: FREG (starting FP register),
704    //   56:23: reserved (0), 22:16: COUNT (number of registers),
705    //   15:3: TARGET (destination Minion ID), 2: reserved (0), 1:0: 00.
706    let xs: u64 = ((freg   as u64 & 0x1F)  << 57)
707               |  ((count  as u64 & 0x7F)  << 16)
708               |  ((target as u64 & 0x1FFF) << 3);
709    // bits 1:0 = 00 (TensorSend) -- naturally zero.
710    unsafe {
711        asm!(
712            concat!("csrrw x0, ", stringify!(0x800), ", {xs}"),
713            xs = in(reg) xs,
714            options(nostack),
715        );
716    }
717}
718
719/// Initiate an asynchronous TensorRecv.
720///
721/// Receives `count` FP registers from the Minion identified by `source` and
722/// combines them with the local FP registers starting at `freg` using the
723/// operation specified by `funct`. This is the matching receive primitive for
724/// [`tensor_send`].
725///
726/// # Parameters
727/// - `freg`:   Starting local FP register index (0..=31).
728/// - `funct`:  Combination operation applied to received and local values.
729/// - `count`:  Number of FP registers to receive (0..=127); must match the
730///   sender's `count`.
731/// - `source`: Source Minion ID (SOURCE field, bits 15:3 of xs).
732///
733/// # Safety
734/// - The partner hart must have called [`tensor_send`] before this retires.
735/// - Must be called from the primary hart of the Minion.
736#[inline(always)]
737pub unsafe fn tensor_recv(freg: u8, funct: ReduceFunct, count: u8, source: u16) {
738    // xs bit layout (PRM Table 9-8, TensorRecv):
739    //   63:62: reserved (0), 61:57: FREG, 27:24: FUNCT, 23: reserved (0),
740    //   22:16: COUNT, 15:3: SOURCE, 2: reserved (0), 1:0: 01 (TensorRecv).
741    let xs: u64 = ((freg   as u64 & 0x1F)  << 57)
742               |  ((funct  as u64 & 0xF)   << 24)
743               |  ((count  as u64 & 0x7F)  << 16)
744               |  ((source as u64 & 0x1FFF) << 3)
745               |  1_u64;  // bits 1:0 = 01 (TensorRecv)
746    unsafe {
747        asm!(
748            concat!("csrrw x0, ", stringify!(0x800), ", {xs}"),
749            xs = in(reg) xs,
750            options(nostack),
751        );
752    }
753}
754
755/// Initiate an asynchronous TensorStore from the FP register file to memory.
756///
757/// Stores `arows + 1` rows of 64 bytes each (16 f32 per row, occupying two
758/// consecutive 256-bit FP registers) to memory. Row `i` is stored to
759/// address `addr + i * stride`, reading from FP registers f[2i] and f[2i+1].
760/// The operation is asynchronous: call [`crate::fence`] after to guarantee
761/// visibility to other agents before the kernel returns.
762///
763/// # Parameters
764/// - `addr`:  64-byte aligned virtual address of the first C row in memory.
765/// - `arows`: number of C rows to store minus one (ROWS field, 0..=15).
766/// - `stride`: row stride of C in bytes (64-byte aligned); placed in x31.
767///
768/// # Safety
769/// - `addr` must be 64-byte aligned and point to `(arows + 1) * stride` bytes
770///   of writable device memory.
771/// - `tensor_wait(TensorEvent::Fma)` must have been called first.
772/// - Must be called from the primary hart of the Minion.
773#[inline(always)]
774pub unsafe fn tensor_store(addr: usize, arows: u8, stride: u64) {
775    debug_assert!(addr.is_multiple_of(64), "tensor_store: addr must be 64-byte aligned");
776    // xs bit layout (PRM Table 9-7):
777    //   63:62: STEP=0 (fstep=1; row i uses f[2i] and f[2i+1]),
778    //   61:57: FREG=0 (start at f0),
779    //   56:55: SIZE=3 (64 bytes = 16 f32 per row, two 256-bit registers),
780    //   54:51: ROWS,
781    //   50:49: COOP=0 (no cooperative multi-hart store),
782    //   48:   0 (store from FP registers, not from Scp),
783    //   47:4: ADDR >> 4 (addr is 64B-aligned, so addr & !0xF == addr),
784    //   3:0:  0000.
785    // Zero fields (STEP=0 at 63:62, FREG=0 at 61:57, COOP=0 at 50:49,
786    // source=FP-registers at 48) are left as the natural zero of u64.
787    let xs: u64 = (3_u64 << 55)                        // SIZE=3 (64B/row)
788               |  ((arows as u64) << 51)               // ROWS
789               |  (addr as u64 & !0xF_usize as u64);   // ADDR[47:4]; addr is 64B-aligned
790    // x31 carries the C row stride; TensorStore uses bits [47:4] of x31.
791    unsafe {
792        asm!(
793            "mv t6, {stride}",
794            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
795            stride = in(reg) stride,
796            xs     = in(reg) xs,
797            out("t6") _,
798            options(nostack),
799        );
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    /// Verify the FMA32 xs bit packing for a standard full-tile configuration:
808    /// BCOLS=3, AROWS=15, ACOLS=15, AOFFSET=0, TENB=1, BSTART=0, ASTART=0.
809    #[test]
810    fn fma32_xs_full_tile() {
811        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
812        // BCOLS=3 at bits 56:55 -> 3 << 55
813        assert_eq!(xs & (0x3 << 55), 3 << 55);
814        // AROWS=15 at bits 54:51 -> 15 << 51
815        assert_eq!(xs & (0xF << 51), 15 << 51);
816        // ACOLS=15 at bits 50:47
817        assert_eq!(xs & (0xF << 47), 15 << 47);
818        // TENB=1 at bit 20
819        assert_eq!(xs & (1 << 20), 1 << 20);
820        // MUL=0, MSK=0
821        assert_eq!(xs & 1, 0);
822        assert_eq!(xs >> 63, 0);
823    }
824
825    /// Verify mul_only sets bit 0.
826    #[test]
827    fn fma32_xs_mul_only() {
828        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, true, false);
829        assert_eq!(xs & 1, 1);
830    }
831
832    /// Verify that TensorEvent discriminants match PRM Table 9-2.
833    #[test]
834    fn tensor_event_discriminants() {
835        assert_eq!(TensorEvent::Load0        as u64, 0);
836        assert_eq!(TensorEvent::Load1        as u64, 1);
837        assert_eq!(TensorEvent::LoadL2_0     as u64, 2);
838        assert_eq!(TensorEvent::LoadL2_1     as u64, 3);
839        assert_eq!(TensorEvent::Prefetch0    as u64, 4);
840        assert_eq!(TensorEvent::Prefetch1    as u64, 5);
841        assert_eq!(TensorEvent::CacheOp      as u64, 6);
842        assert_eq!(TensorEvent::Fma          as u64, 7);
843        assert_eq!(TensorEvent::Store        as u64, 8);
844        assert_eq!(TensorEvent::TensorReduce as u64, 9);
845        assert_eq!(TensorEvent::TensorQuant  as u64, 10);
846    }
847
848    /// Verify TensorLoad xs encoding for addr=0x1000, start=0, rows=15.
849    #[test]
850    fn tensor_load_xs_encoding() {
851        let addr: usize = 0x0080_0000_1000; // 64B-aligned
852        let start: u8 = 0;
853        let rows: u8 = 15;
854        let xs: u64 = ((start as u64 & 0x3F) << 53)
855                   |  (addr as u64)
856                   |  (rows as u64 & 0xF);
857        // START field (bits 58:53) = 0
858        assert_eq!((xs >> 53) & 0x3F, 0);
859        // bit 52 = 0 (TensorLoad, not TensorLoadB)
860        assert_eq!((xs >> 52) & 1, 0);
861        // ROWS = 15
862        assert_eq!(xs & 0xF, 15);
863        // ADDR embedded at bits 47:6 (addr = 0x80_0000_1000, bits fit in 47:6)
864        let addr_bits = addr as u64 & 0x0000_FFFF_FFFF_FFFF;
865        assert_eq!(xs & addr_bits, addr_bits);
866    }
867
868    /// Verify that fma16a32_xs differs from fma32_xs only in bits 3:1.
869    #[test]
870    fn fma16a32_xs_tensortype() {
871        let xs32  = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
872        let xs16  = fma16a32_xs(3, 15, 15, 0, true, 0, 0, false, false);
873        // bits 3:1 must be 001 (value 2) for FMA16A32
874        assert_eq!((xs16 >> 1) & 0x7, 1);
875        // all other bits identical
876        assert_eq!(xs32 & !(0x7 << 1), xs16 & !(0x7 << 1));
877    }
878
879    /// Verify ima8a32_xs bits 3:1 = 011 and the DST/UA/UB fields.
880    #[test]
881    fn ima8a32_xs_fields() {
882        let xs = ima8a32_xs(
883            /*bcols*/      3,
884            /*arows*/     15,
885            /*acols*/     15,
886            /*aoffset*/    0,
887            /*b_in_mem*/ false,
888            /*bstart*/     0,
889            /*astart*/     0,
890            /*dst_fp*/  true,
891            /*b_unsigned*/ true,
892            /*a_unsigned*/ true,
893            /*mul_only*/ false,
894            /*use_mask*/ false,
895        );
896        // TensorType bits 3:1 = 011
897        assert_eq!((xs >> 1) & 0x7, 3);
898        // DST = 1 at bit 23
899        assert_eq!((xs >> 23) & 1, 1);
900        // UB = 1 at bit 22
901        assert_eq!((xs >> 22) & 1, 1);
902        // UA = 1 at bit 21
903        assert_eq!((xs >> 21) & 1, 1);
904        // TENB = 0 (b_in_mem = false)
905        assert_eq!((xs >> 20) & 1, 0);
906        // BCOLS, AROWS, ACOLS
907        assert_eq!((xs >> 55) & 0x3, 3);
908        assert_eq!((xs >> 51) & 0xF, 15);
909        assert_eq!((xs >> 47) & 0xF, 15);
910    }
911
912    /// Verify ima8a32_xs with b_in_mem=true sets TENB bit.
913    #[test]
914    fn ima8a32_xs_b_in_mem() {
915        let xs = ima8a32_xs(0, 0, 0, 0, true, 0, 0, false, false, false, false, false);
916        assert_eq!((xs >> 20) & 1, 1);  // TENB = 1 (memory path)
917    }
918
919    /// Verify tensor_store_from_scp xs: bit 48 = 1, STEP, START, ROWS, ADDR.
920    #[test]
921    fn store_from_scp_xs_fields() {
922        let addr: usize = 0x0080_0000_2000;  // 64B-aligned
923        let xs: u64 = (((4_u64 - 1) & 0x3) << 62)  // step=4 -> STEP=3
924                   |  ((12_u64 & 0x3F) << 56)        // start=12
925                   |  ((7_u64  & 0xF)  << 51)        // rows=7
926                   |  (1_u64           << 48)         // source = scratchpad
927                   |  (addr as u64 & 0x0000_FFFF_FFFF_FFC0_usize as u64);
928        // bit 48 = 1 (TensorStoreFromScp discriminator)
929        assert_eq!((xs >> 48) & 1, 1);
930        // STEP = 3 (step - 1) at bits 63:62
931        assert_eq!(xs >> 62, 3);
932        // START = 12 at bits 61:56
933        assert_eq!((xs >> 56) & 0x3F, 12);
934        // ROWS = 7 at bits 54:51
935        assert_eq!((xs >> 51) & 0xF, 7);
936        // ADDR embedded at bits 47:6 (addr is 64B-aligned)
937        assert_eq!(xs & addr as u64, addr as u64);
938    }
939
940    /// Verify tensor_send xs: bits 1:0 = 00, FREG, COUNT, TARGET fields.
941    #[test]
942    fn tensor_send_xs_fields() {
943        let freg: u8 = 16;
944        let count: u8 = 8;
945        let target: u16 = 5;
946        let xs: u64 = ((freg   as u64 & 0x1F)   << 57)
947                   |  ((count  as u64 & 0x7F)   << 16)
948                   |  ((target as u64 & 0x1FFF) << 3);
949        // bits 1:0 = 00 (TensorSend)
950        assert_eq!(xs & 0x3, 0);
951        // FREG at bits 61:57
952        assert_eq!((xs >> 57) & 0x1F, 16);
953        // COUNT at bits 22:16
954        assert_eq!((xs >> 16) & 0x7F, 8);
955        // TARGET at bits 15:3
956        assert_eq!((xs >> 3) & 0x1FFF, 5);
957    }
958
959    /// Verify tensor_recv xs: bits 1:0 = 01, FUNCT field.
960    #[test]
961    fn tensor_recv_xs_fields() {
962        let xs: u64 = ((4_u64 & 0x1F)   << 57)   // freg=4
963                   |  ((ReduceFunct::Fadd as u64 & 0xF) << 24)  // FUNCT=0 (FADD)
964                   |  ((16_u64 & 0x7F)  << 16)   // count=16
965                   |  ((3_u64 & 0x1FFF) << 3)    // source=3
966                   |  1_u64;                      // bits 1:0 = 01 (TensorRecv)
967        // bits 1:0 = 01
968        assert_eq!(xs & 0x3, 1);
969        // FUNCT = 0 (FADD) at bits 27:24
970        assert_eq!((xs >> 24) & 0xF, 0);
971        // FREG at bits 61:57
972        assert_eq!((xs >> 57) & 0x1F, 4);
973    }
974
975    /// Verify TensorStore xs encoding: STEP=0, FREG=0, SIZE=3.
976    #[test]
977    fn tensor_store_xs_fields() {
978        let addr: usize = 0x0080_0000_2000; // 64B-aligned
979        let arows: u8 = 7;
980        let xs: u64 = (3_u64 << 55)              // SIZE=3
981                   |  ((arows as u64) << 51)
982                   |  (addr as u64 & !0xF_usize as u64);
983        // SIZE = 3 at bits 56:55
984        assert_eq!((xs >> 55) & 0x3, 3);
985        // ROWS = 7 at bits 54:51
986        assert_eq!((xs >> 51) & 0xF, 7);
987        // STEP = 0 at bits 63:62
988        assert_eq!(xs >> 62, 0);
989        // FREG = 0 at bits 61:57
990        assert_eq!((xs >> 57) & 0x1F, 0);
991        // ADDR is embedded (addr is 64B-aligned, so !0xF == addr)
992        assert_eq!(xs & (addr as u64), addr as u64);
993    }
994}