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: `riscv64gc` 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!(
243        addr.is_multiple_of(64),
244        "tensor_load_l2: addr must be 64-byte aligned"
245    );
246    // xs layout is identical to TensorLoad; only the CSR address differs.
247    let xs: u64 = ((start as u64 & 0x3F) << 53) | (addr as u64) | (rows as u64 & 0xF);
248    unsafe {
249        asm!(
250            "mv t6, {stride}",
251            concat!("csrrw x0, ", stringify!(0x85F), ", {xs}"),
252            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
253            xs     = in(reg) xs,
254            out("x31") _,
255            options(nostack),
256        );
257    }
258}
259
260/// Write the per-row enable mask for the next TensorFMA.
261///
262/// Bit `i` in `mask` enables row `i` of the A tile. Setting bit `i = 0`
263/// suppresses the update to C row `i` (useful for partial M tiles when the
264/// mask register is more convenient than setting AROWS). For most uses,
265/// leave the mask at its reset value of all-ones and control the tile size
266/// via the AROWS field in [`tensor_fma32`].
267#[inline(always)]
268pub fn set_tensor_mask(mask: u16) {
269    let xs: u64 = mask as u64;
270    unsafe {
271        asm!(
272            concat!("csrrw x0, ", stringify!(0x805), ", {xs}"),
273            xs = in(reg) xs,
274            options(nomem, nostack, preserves_flags),
275        );
276    }
277}
278
279/// Initiate an asynchronous TensorLoad from memory into the L1 scratchpad.
280///
281/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into
282/// L1 scratchpad lines `start` through `start + rows`. Row `i` is read from
283/// address `addr + i * stride`. The operation is asynchronous: call
284/// `tensor_wait(TensorEvent::Load0)` (or `Load1` if `id = true`) before
285/// reading the scratchpad in a subsequent [`tensor_fma32`].
286///
287/// # Parameters
288/// - `addr`: 64-byte aligned virtual address of the first row in memory.
289/// - `start`: L1 scratchpad starting line index (0..=47).
290/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
291///   Loads `rows + 1` cache lines.
292/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
293/// - `stride`: row stride in bytes (64-byte aligned); placed in x31 by this
294///   function immediately before the CSRRW instruction.
295///
296/// # Safety
297/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` valid,
298///   readable bytes of device memory.
299/// - Must be called from the primary hart of the Minion (mhartid & 1 == 0).
300#[inline(always)]
301pub unsafe fn tensor_load(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
302    debug_assert!(
303        addr.is_multiple_of(64),
304        "tensor_load: addr must be 64-byte aligned"
305    );
306    // xs bit layout (PRM Table 9-5):
307    //   63: MSK=0, 62: COOP=0, 61:59=000 (TensorLoad variant),
308    //   58:53=START (6-bit scratchpad line index),
309    //   52=0 (TensorLoad, not TensorLoadB),
310    //   51:48=0 (reserved), 47:6=ADDR>>6 (addr is 64B-aligned so bits 5:0 = 0),
311    //   5:4=0 (reserved), 3:0=ROWS.
312    let xs: u64 = ((start as u64 & 0x3F) << 53)
313               |  (addr as u64)           // bits 47:6; addr is 64B-aligned so addr & !63 == addr
314               |  (rows as u64 & 0xF);
315    // x31 (t6) carries the row stride; the hardware reads it implicitly.
316    unsafe {
317        asm!(
318            "mv t6, {stride}",
319            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
320            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
321            xs     = in(reg) xs,
322            out("x31") _,
323            options(nostack),
324        );
325    }
326}
327
328/// Initiate an asynchronous TensorLoadInterleave16 from memory into the L1 scratchpad.
329///
330/// Identical to [`tensor_load`] except that the hardware automatically
331/// interleaves consecutive fp16 row pairs during the DMA transfer, producing
332/// the 2-row-interleaved layout that [`tensor_fma16a32`] expects in the
333/// scratchpad. This avoids a host-side pre-packing pass for A tiles when the
334/// source data is plain row-major fp16 in DRAM.
335///
336/// The distinction from [`tensor_load_b`]: TensorLoadInterleave16 writes to
337/// the L1 scratchpad (bit 52 = 0) and is suitable for A tiles passed to
338/// [`tensor_fma16a32`] with `tenb = false`. The TenB register-file path has
339/// no hardware interleave mode; B must be pre-packed host-side.
340///
341/// # Parameters
342/// - `addr`: 64-byte aligned virtual address of the first row in memory.
343/// - `start`: L1 scratchpad starting line index (0..=47).
344/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
345///   Loads `rows + 1` cache lines.
346/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
347/// - `stride`: row stride in bytes (64-byte aligned); placed in x31.
348///
349/// # Safety
350/// Same alignment and primary-hart constraints as [`tensor_load`].
351#[inline(always)]
352pub unsafe fn tensor_load_interleave16(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
353    debug_assert!(
354        addr.is_multiple_of(64),
355        "tensor_load_interleave16: addr must be 64-byte aligned"
356    );
357    // xs bit layout (PRM Table 9-5, TensorLoadInterleave16 variant):
358    //   63: MSK=0, 62: COOP=0, 61:59=010 (Interleave16 variant selector),
359    //   58:53=START (6-bit scratchpad line index),
360    //   52=0 (scratchpad target, not TenB register file),
361    //   51:48=0 (reserved), 47:6=ADDR>>6, 5:4=0 (reserved), 3:0=ROWS.
362    let xs: u64 = (0b010_u64              << 59)  // Interleave16 variant
363               |  ((start as u64 & 0x3F)  << 53)
364               |  (addr as u64)
365               |  (rows as u64 & 0xF);
366    unsafe {
367        asm!(
368            "mv t6, {stride}",
369            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
370            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
371            xs     = in(reg) xs,
372            out("x31") _,
373            options(nostack),
374        );
375    }
376}
377
378/// Initiate an asynchronous TensorLoadB from memory into the TenB register file.
379///
380/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into the
381/// dedicated TenB buffer. This forward-pairs with the next [`tensor_fma32`]
382/// call that uses `tenb = true`; the FMA waits internally for the load to
383/// complete, so no explicit `tensor_wait` is needed between LoadB and FMA.
384///
385/// # Parameters
386/// - `addr`: 64-byte aligned virtual address of the first B row in memory.
387/// - `rows`: B rows to load minus one (ACOLS of the subsequent FMA, 0..=15).
388/// - `coop`: set for cooperative multi-hart loading (advanced; leave false).
389/// - `stride`: row stride of B in bytes (64-byte aligned); placed in x31.
390/// - `id`: load event identifier placed in bit 0 of x31 (false = `Load0`,
391///   true = `Load1`). Use `Load1` when a `tensor_load` with `id: false` is
392///   also in flight, so that `tensor_wait(Load0)` waits only for the A tile
393///   and not for the B DMA (which forward-pairs with the FMA anyway).
394///
395/// # Note: TenB path has no hardware interleave variant
396///
397/// The TenB register-file path (xs bit 52 = 1) does not support hardware
398/// interleaving of consecutive fp16 rows. B must be pre-packed host-side into
399/// the 2-row-interleaved layout that FMA16A32 expects before upload.
400/// [`tensor_load_interleave16`] (xs bits 61:59 = 010, xs bit 52 = 0)
401/// interleaves from plain row-major fp16 in DRAM into the L1 scratchpad, but
402/// it targets the scratchpad path only, not the TenB register file; there is
403/// no interleave variant for the TenB path.
404///
405/// # Safety
406/// Same alignment and primary-hart constraints as [`tensor_load`].
407#[inline(always)]
408pub unsafe fn tensor_load_b(addr: usize, rows: u8, coop: bool, stride: u64, id: bool) {
409    debug_assert!(
410        addr.is_multiple_of(64),
411        "tensor_load_b: addr must be 64-byte aligned"
412    );
413    // xs bit layout (PRM Table 9-6):
414    //   63: MSK=0, 62: COOP, 61:53=0 (reserved),
415    //   52=1 (TensorLoadB distinguisher),
416    //   51:48=0 (reserved), 47:6=ADDR>>6, 5:4=0, 3:0=ROWS.
417    // x31 bit 0 = ID (identical mechanism to TensorLoad; PRM Chapter 9).
418    let xs: u64 = ((coop as u64)  << 62)
419               |  (1_u64          << 52)
420               |  (addr as u64)           // 64B-aligned: bits 47:6 correct
421               |  (rows as u64 & 0xF);
422    unsafe {
423        asm!(
424            "mv t6, {stride}",
425            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
426            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
427            xs     = in(reg) xs,
428            out("x31") _,
429            options(nostack),
430        );
431    }
432}
433
434/// Build the xs value for a TensorFMA32 instruction.
435///
436/// The FMA computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when
437/// `mul_only = true`), accumulating into the FP register file.
438///
439/// # Parameters
440/// - `bcols`:    B column groups minus one (BCOLS field, 0..=3; output columns
441///   = 4*(bcols+1), e.g. 3 -> 16 columns).
442/// - `arows`:    A tile rows minus one (AROWS field, 0..=15).
443/// - `acols`:    A tile columns minus one (ACOLS field, 0..=15); also the
444///   number of B rows loaded by the preceding [`tensor_load_b`].
445/// - `aoffset`:  byte offset within each scratchpad line where A row data
446///   begins, in 4-byte units (AOFFSET, 0..=15). Use 0 when A columns start
447///   at the beginning of a cache line.
448/// - `tenb`:     `true` to read B from the TenB register file (filled by the
449///   preceding [`tensor_load_b`]); `false` to read from the L1 scratchpad
450///   at `bstart`.
451/// - `bstart`:   scratchpad line index of B (ignored when `tenb = true`).
452/// - `astart`:   scratchpad line index of A (ASTART field, 0..=47).
453/// - `mul_only`: `true` for C = A*B (ignore existing FP register values);
454///   `false` for C += A*B (accumulate into current FP registers).
455/// - `use_mask`: apply the tensor_mask row-enable register.
456#[must_use = "the returned xs value must be passed to tensor_fma32; discarding it issues no instruction"]
457#[allow(clippy::too_many_arguments)]
458#[inline]
459pub fn fma32_xs(
460    bcols: u8,
461    arows: u8,
462    acols: u8,
463    aoffset: u8,
464    tenb: bool,
465    bstart: u8,
466    astart: u8,
467    mul_only: bool,
468    use_mask: bool,
469) -> u64 {
470    // xs bit layout (PRM Table 9-4):
471    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
472    //   50:47: ACOLS, 46:43: AOFFSET, 42:21: reserved (0), 20: TENB,
473    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
474    //   9:4: ASTART, 3:1: 000 (FMA32 TensorType), 0: MUL.
475    ((use_mask as u64)       << 63)
476  | ((bcols   as u64 & 0x3)  << 55)
477  | ((arows   as u64 & 0xF)  << 51)
478  | ((acols   as u64 & 0xF)  << 47)
479  | ((aoffset as u64 & 0xF)  << 43)
480  | ((tenb    as u64)        << 20)
481  | ((bstart  as u64 & 0x3F) << 12)
482  | ((astart  as u64 & 0x3F) <<  4)
483  // bits 3:1 = 000 (FMA32 TensorType selector)
484  | (mul_only as u64)
485}
486
487/// Initiate an asynchronous TensorFMA32.
488///
489/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma32_xs`]. The
490/// operation is asynchronous: call `tensor_wait(TensorEvent::Fma)` before
491/// reading the FP register file or issuing a subsequent [`tensor_store`].
492///
493/// # Safety
494/// - The L1 scratchpad must be fully populated (TensorLoad with subsequent
495///   `tensor_wait(Load0)`) before this call when `tenb = false`, or
496///   equivalently [`tensor_load_b`] must have been issued before this call
497///   for the TenB path.
498/// - Must be called from the primary hart of the Minion.
499#[inline(always)]
500pub unsafe fn tensor_fma32(xs: u64) {
501    unsafe {
502        asm!(
503            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
504            xs = in(reg) xs,
505            options(nostack),
506        );
507    }
508}
509
510/// Build the xs value for a TensorFMA16A32 instruction.
511///
512/// Computes C += A * B with fp16 inputs and fp32 accumulation. The hardware
513/// processes two K-columns per clock in a fused 3-way addition that is not
514/// IEEE754-equivalent to two separate adds: the internal partial sum is
515/// unrounded, and the final result is rounded toward zero (RTZ), not to
516/// nearest. This introduces a systematic truncation bias. Measured RMS
517/// relative error is approximately 2.6e-4 against an fp32 reference, flat
518/// between K=2048 and K=4096; input-rounding dominates the RTZ bias at those
519/// depths. The xs bit layout is identical to [`fma32_xs`] except bits 3:1 =
520/// `001` (FMA16A32 TensorType selector).
521///
522/// # Tile geometry -- ACOLS differs from FMA32
523///
524/// The ACOLS field contracts a different number of K elements than in
525/// [`fma32_xs`]:
526///
527/// - `acols = n` means K = 2*(n+1) fp16 pairs (two K-columns per step).
528///   For example, `acols=0` -> K=2, `acols=15` -> K=32.
529/// - Each A row in the L1 scratchpad occupies `(acols+1)*4` bytes,
530///   holding `(acols+1)*2` fp16 values (two per group of ACOLS+1 groups).
531/// - The paired [`tensor_load_b`] must be issued with `rows = acols`; the
532///   hardware fires `tensor_error[6]` if `LoadB.ROWS != ACOLS`.
533///
534/// # Parameters
535/// (same names as [`fma32_xs`]; `tenb = true` selects the TenB register file
536/// for B.)
537#[must_use = "the returned xs value must be passed to tensor_fma16a32; \
538              discarding it issues no instruction"]
539#[allow(clippy::too_many_arguments)]
540#[inline]
541pub fn fma16a32_xs(
542    bcols: u8,
543    arows: u8,
544    acols: u8,
545    aoffset: u8,
546    tenb: bool,
547    bstart: u8,
548    astart: u8,
549    mul_only: bool,
550    use_mask: bool,
551) -> u64 {
552    // xs bit layout (PRM Table 9-4, TensorFMA16A32 variant):
553    //   identical to TensorFMA32 (fma32_xs) except bits 3:1 = 001.
554    ((use_mask as u64)       << 63)
555  | ((bcols   as u64 & 0x3)  << 55)
556  | ((arows   as u64 & 0xF)  << 51)
557  | ((acols   as u64 & 0xF)  << 47)
558  | ((aoffset as u64 & 0xF)  << 43)
559  | ((tenb    as u64)        << 20)
560  | ((bstart  as u64 & 0x3F) << 12)
561  | ((astart  as u64 & 0x3F) <<  4)
562  | (1_u64                    <<  1)  // bits 3:1 = 001 (FMA16A32 TensorType)
563  | (mul_only as u64)
564}
565
566/// Initiate an asynchronous TensorFMA16A32.
567///
568/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma16a32_xs`].
569/// The hardware selects the FMA16A32 path via bits 3:1 = `001` in xs.
570/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
571///
572/// # Safety
573/// Same constraints as [`tensor_fma32`].
574#[inline(always)]
575pub unsafe fn tensor_fma16a32(xs: u64) {
576    unsafe {
577        asm!(
578            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
579            xs = in(reg) xs,
580            options(nostack),
581        );
582    }
583}
584
585/// Build the xs value for a TensorIMA8A32 instruction.
586///
587/// Computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when `mul_only = true`),
588/// where A and B hold 8-bit integer elements and C accumulates as 32-bit signed
589/// integers. The A matrix is `(AROWS+1) x (ACOLS+1)*4` int8 elements; the B
590/// matrix is `(ACOLS+1)*4 x (BCOLS+1)*16` int8 elements (interleaved 4 columns
591/// at a time); the output is `(AROWS+1) x (BCOLS+1)*4` int32 values.
592///
593/// # Parameters
594/// - `bcols`:      B column groups minus one (BCOLS, 0..=3; output columns = 4*(bcols+1)).
595/// - `arows`:      A tile rows minus one (AROWS, 0..=15).
596/// - `acols`:      A tile column groups minus one (ACOLS, 0..=15). Each group
597///   contains 4 int8 K-elements, so `acols = n` contracts K = 4*(n+1) rows
598///   (e.g. `acols=0` -> K=4, `acols=15` -> K=64). Each A row in the L1
599///   scratchpad occupies `(acols+1)*4` bytes.
600/// - `aoffset`:    Byte offset within each scratchpad line for A data, in 4-byte units
601///   (AOFFSET, 0..=15).
602/// - `b_in_mem`:   `true` if B is transferred via the memory DMA path; `false` for L1
603///   scratchpad. (TENB = 1 means memory for IMA8A32, unlike FMA where TENB=1 is TenB
604///   register file.)
605/// - `bstart`:     Starting scratchpad line for B; ignored when `b_in_mem = true`.
606/// - `astart`:     Starting scratchpad line for A (ASTART, 0..=47).
607/// - `dst_fp`:     `true` to write the int32 result to the FP register file;
608///   `false` to write to the TenC register file. (DST, xs bit 23)
609/// - `b_unsigned`: `true` if B elements are unsigned; `false` for signed.
610/// - `a_unsigned`: `true` if A elements are unsigned; `false` for signed.
611/// - `mul_only`:   `true` for C = A*B; `false` for C += A*B.
612/// - `use_mask`:   Apply the tensor_mask row-enable register.
613#[must_use = "the returned xs value must be passed to tensor_ima8a32; \
614              discarding it issues no instruction"]
615#[allow(clippy::too_many_arguments)]
616#[inline]
617pub fn ima8a32_xs(
618    bcols: u8,
619    arows: u8,
620    acols: u8,
621    aoffset: u8,
622    b_in_mem: bool,
623    bstart: u8,
624    astart: u8,
625    dst_fp: bool,
626    b_unsigned: bool,
627    a_unsigned: bool,
628    mul_only: bool,
629    use_mask: bool,
630) -> u64 {
631    // xs bit layout (PRM Table 9-4, TensorIMA8A32 variant):
632    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
633    //   50:47: ACOLS, 46:43: AOFFSET, 42:24: reserved (0),
634    //   23: DST (0=TenC, 1=FP registers), 22: UB, 21: UA,
635    //   20: TENB (0=L1 scratchpad, 1=memory path for IMA8A32),
636    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
637    //   9:4: ASTART, 3:1: 011 (IMA8A32 TensorType), 0: MUL.
638    ((use_mask   as u64)        << 63)
639  | ((bcols      as u64 & 0x3)  << 55)
640  | ((arows      as u64 & 0xF)  << 51)
641  | ((acols      as u64 & 0xF)  << 47)
642  | ((aoffset    as u64 & 0xF)  << 43)
643  | ((dst_fp     as u64)        << 23)
644  | ((b_unsigned as u64)        << 22)
645  | ((a_unsigned as u64)        << 21)
646  | ((b_in_mem   as u64)        << 20)
647  | ((bstart     as u64 & 0x3F) << 12)
648  | ((astart     as u64 & 0x3F) <<  4)
649  | (3_u64                       <<  1)  // bits 3:1 = 011 (IMA8A32 TensorType)
650  | (mul_only    as u64)
651}
652
653/// Initiate an asynchronous TensorIMA8A32.
654///
655/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`ima8a32_xs`].
656/// The hardware selects the integer-GEMM path via bits 3:1 = `011` in xs.
657/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
658///
659/// # Safety
660/// Same constraints as [`tensor_fma32`].
661#[inline(always)]
662pub unsafe fn tensor_ima8a32(xs: u64) {
663    unsafe {
664        asm!(
665            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
666            xs = in(reg) xs,
667            options(nostack),
668        );
669    }
670}
671
672/// Initiate an asynchronous TensorStoreFromScp to memory from the L1 scratchpad.
673///
674/// Stores `rows + 1` 64-byte scratchpad lines to memory, bypassing the L1 data
675/// cache and L2 cache. Consecutive scratchpad lines are spaced `step` lines apart
676/// (so `step = 1` stores consecutive lines); consecutive destination rows are
677/// spaced `stride` bytes apart.
678///
679/// # Parameters
680/// - `addr`:   64-byte aligned virtual address of the first destination row.
681/// - `rows`:   Number of rows to store minus one (ROWS, 0..=15).
682/// - `start`:  Starting L1 scratchpad cache line (0..=47).
683/// - `step`:   Scratchpad line stride (1..=4); encoded as STEP = step - 1.
684/// - `stride`: Destination row stride in bytes; placed in x31. Low 6 bits are
685///   ignored by the hardware (rows are 64-byte aligned in memory).
686///
687/// # Safety
688/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` bytes of
689///   writable device memory.
690/// - Must be called from the primary hart of the Minion.
691#[inline(always)]
692pub unsafe fn tensor_store_from_scp(addr: usize, rows: u8, start: u8, step: u8, stride: u64) {
693    debug_assert!(
694        addr.is_multiple_of(64),
695        "tensor_store_from_scp: addr must be 64-byte aligned"
696    );
697    // xs bit layout (PRM Table 9-7, TensorStoreFromScp):
698    //   63:62: STEP (step-1; scratchpad line stride), 61:56: START (first
699    //   scratchpad line), 55: reserved (0), 54:51: ROWS (rows-1), 50:49:
700    //   reserved (0), 48: 1 (TensorStoreFromScp discriminator, bit 48=0 is
701    //   TensorStore), 47:6: ADDR (virtual address with 6 low-order bits
702    //   omitted; addr is 64B-aligned), 5:0: reserved (0).
703    // x31 carries the destination row stride; low 6 bits are ignored by hardware.
704    let xs: u64 = (((step as u64).saturating_sub(1) & 0x3) << 62)
705               |  ((start as u64 & 0x3F) << 56)
706               |  ((rows  as u64 & 0xF)  << 51)
707               |  (1_u64                 << 48)         // source = L1 scratchpad
708               |  (addr as u64 & 0x0000_FFFF_FFFF_FFC0_usize as u64); // ADDR[47:6]
709    unsafe {
710        asm!(
711            "mv t6, {stride}",
712            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
713            stride = in(reg) stride,
714            xs     = in(reg) xs,
715            out("x31") _,
716            options(nostack),
717        );
718    }
719}
720
721/// Reduction function selector for [`tensor_recv`].
722///
723/// Specifies how the received values are combined with the values already held
724/// in the destination FP registers. (PRM Table 9-8, FUNCT field.)
725#[repr(u8)]
726#[derive(Clone, Copy, Debug, PartialEq, Eq)]
727pub enum ReduceFunct {
728    /// C[i] = C[i] + src[i]  (fp32 addition)
729    Fadd = 0,
730    /// C[i] = fmax(C[i], src[i])
731    Fmax = 2,
732    /// C[i] = fmin(C[i], src[i])
733    Fmin = 3,
734    /// C[i] = C[i] + src[i]  (integer addition on bit pattern)
735    Add = 4,
736    /// C[i] = max(C[i], src[i])  (signed 32-bit integer comparison)
737    Max = 6,
738    /// C[i] = min(C[i], src[i])  (signed 32-bit integer comparison)
739    Min = 7,
740    /// C[i] = src[i]             (unconditional move)
741    Move = 8,
742}
743
744/// Initiate an asynchronous TensorSend.
745///
746/// Pushes `count` consecutive FP registers starting at `freg` from this hart
747/// to hart 0 of the Minion identified by `target`. The partner hart must issue
748/// a matching [`tensor_recv`]. This is the low-level primitive for hart-to-hart
749/// reduction without software memory traffic.
750///
751/// # Parameters
752/// - `freg`:   Starting FP register index (0..=31).
753/// - `count`:  Number of FP registers to send (COUNT field, 0..=127).
754/// - `target`: Destination Minion ID (TARGET field, bits 15:3 of xs).
755///
756/// # Safety
757/// - The partner hart must call [`tensor_recv`] with the matching `source` and
758///   `count` before the send retires.
759/// - Must be called from the primary hart of the Minion.
760#[inline(always)]
761pub unsafe fn tensor_send(freg: u8, count: u8, target: u16) {
762    // xs bit layout (PRM Table 9-8, TensorSend):
763    //   63:62: reserved (0), 61:57: FREG (starting FP register),
764    //   56:23: reserved (0), 22:16: COUNT (number of registers),
765    //   15:3: TARGET (destination Minion ID), 2: reserved (0), 1:0: 00.
766    let xs: u64 = ((freg as u64 & 0x1F) << 57)
767        | ((count as u64 & 0x7F) << 16)
768        | ((target as u64 & 0x1FFF) << 3);
769    // bits 1:0 = 00 (TensorSend) -- naturally zero.
770    unsafe {
771        asm!(
772            concat!("csrrw x0, ", stringify!(0x800), ", {xs}"),
773            xs = in(reg) xs,
774            options(nostack),
775        );
776    }
777}
778
779/// Initiate an asynchronous TensorRecv.
780///
781/// Receives `count` FP registers from the Minion identified by `source` and
782/// combines them with the local FP registers starting at `freg` using the
783/// operation specified by `funct`. This is the matching receive primitive for
784/// [`tensor_send`].
785///
786/// # Parameters
787/// - `freg`:   Starting local FP register index (0..=31).
788/// - `funct`:  Combination operation applied to received and local values.
789/// - `count`:  Number of FP registers to receive (0..=127); must match the
790///   sender's `count`.
791/// - `source`: Source Minion ID (SOURCE field, bits 15:3 of xs).
792///
793/// # Safety
794/// - The partner hart must have called [`tensor_send`] before this retires.
795/// - Must be called from the primary hart of the Minion.
796#[inline(always)]
797pub unsafe fn tensor_recv(freg: u8, funct: ReduceFunct, count: u8, source: u16) {
798    // xs bit layout (PRM Table 9-8, TensorRecv):
799    //   63:62: reserved (0), 61:57: FREG, 27:24: FUNCT, 23: reserved (0),
800    //   22:16: COUNT, 15:3: SOURCE, 2: reserved (0), 1:0: 01 (TensorRecv).
801    let xs: u64 = ((freg as u64 & 0x1F) << 57)
802        | ((funct as u64 & 0xF) << 24)
803        | ((count as u64 & 0x7F) << 16)
804        | ((source as u64 & 0x1FFF) << 3)
805        | 1_u64; // bits 1:0 = 01 (TensorRecv)
806    unsafe {
807        asm!(
808            concat!("csrrw x0, ", stringify!(0x800), ", {xs}"),
809            xs = in(reg) xs,
810            options(nostack),
811        );
812    }
813}
814
815/// Initiate an asynchronous TensorStore from the FP register file to memory.
816///
817/// Stores `arows + 1` rows of 64 bytes each (16 f32 per row, occupying two
818/// consecutive 256-bit FP registers) to memory. Row `i` is stored to
819/// address `addr + i * stride`, reading from FP registers f[2i] and f[2i+1].
820/// The operation is asynchronous: call [`crate::fence`] after to guarantee
821/// visibility to other agents before the kernel returns.
822///
823/// # Parameters
824/// - `addr`:  64-byte aligned virtual address of the first C row in memory.
825/// - `arows`: number of C rows to store minus one (ROWS field, 0..=15).
826/// - `stride`: row stride of C in bytes (64-byte aligned); placed in x31.
827///
828/// # Safety
829/// - `addr` must be 64-byte aligned and point to `(arows + 1) * stride` bytes
830///   of writable device memory.
831/// - `tensor_wait(TensorEvent::Fma)` must have been called first.
832/// - Must be called from the primary hart of the Minion.
833#[inline(always)]
834pub unsafe fn tensor_store(addr: usize, arows: u8, stride: u64) {
835    debug_assert!(
836        addr.is_multiple_of(64),
837        "tensor_store: addr must be 64-byte aligned"
838    );
839    // xs bit layout (PRM Table 9-7):
840    //   63:62: STEP=0 (fstep=1; row i uses f[2i] and f[2i+1]),
841    //   61:57: FREG=0 (start at f0),
842    //   56:55: SIZE=3 (64 bytes = 16 f32 per row, two 256-bit registers),
843    //   54:51: ROWS,
844    //   50:49: COOP=0 (no cooperative multi-hart store),
845    //   48:   0 (store from FP registers, not from Scp),
846    //   47:4: ADDR >> 4 (addr is 64B-aligned, so addr & !0xF == addr),
847    //   3:0:  0000.
848    // Zero fields (STEP=0 at 63:62, FREG=0 at 61:57, COOP=0 at 50:49,
849    // source=FP-registers at 48) are left as the natural zero of u64.
850    let xs: u64 = (3_u64 << 55)                        // SIZE=3 (64B/row)
851               |  ((arows as u64) << 51)               // ROWS
852               |  (addr as u64 & !0xF_usize as u64); // ADDR[47:4]; addr is 64B-aligned
853    // x31 carries the C row stride; TensorStore uses bits [47:4] of x31.
854    unsafe {
855        asm!(
856            "mv t6, {stride}",
857            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
858            stride = in(reg) stride,
859            xs     = in(reg) xs,
860            out("x31") _,
861            options(nostack),
862        );
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869
870    /// Verify the FMA32 xs bit packing for a standard full-tile configuration:
871    /// BCOLS=3, AROWS=15, ACOLS=15, AOFFSET=0, TENB=1, BSTART=0, ASTART=0.
872    #[test]
873    fn fma32_xs_full_tile() {
874        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
875        // BCOLS=3 at bits 56:55 -> 3 << 55
876        assert_eq!(xs & (0x3 << 55), 3 << 55);
877        // AROWS=15 at bits 54:51 -> 15 << 51
878        assert_eq!(xs & (0xF << 51), 15 << 51);
879        // ACOLS=15 at bits 50:47
880        assert_eq!(xs & (0xF << 47), 15 << 47);
881        // TENB=1 at bit 20
882        assert_eq!(xs & (1 << 20), 1 << 20);
883        // MUL=0, MSK=0
884        assert_eq!(xs & 1, 0);
885        assert_eq!(xs >> 63, 0);
886    }
887
888    /// Verify mul_only sets bit 0.
889    #[test]
890    fn fma32_xs_mul_only() {
891        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, true, false);
892        assert_eq!(xs & 1, 1);
893    }
894
895    /// Verify that TensorEvent discriminants match PRM Table 9-2.
896    #[test]
897    fn tensor_event_discriminants() {
898        assert_eq!(TensorEvent::Load0 as u64, 0);
899        assert_eq!(TensorEvent::Load1 as u64, 1);
900        assert_eq!(TensorEvent::LoadL2_0 as u64, 2);
901        assert_eq!(TensorEvent::LoadL2_1 as u64, 3);
902        assert_eq!(TensorEvent::Prefetch0 as u64, 4);
903        assert_eq!(TensorEvent::Prefetch1 as u64, 5);
904        assert_eq!(TensorEvent::CacheOp as u64, 6);
905        assert_eq!(TensorEvent::Fma as u64, 7);
906        assert_eq!(TensorEvent::Store as u64, 8);
907        assert_eq!(TensorEvent::TensorReduce as u64, 9);
908        assert_eq!(TensorEvent::TensorQuant as u64, 10);
909    }
910
911    /// Verify TensorLoad xs encoding for addr=0x1000, start=0, rows=15.
912    #[test]
913    fn tensor_load_xs_encoding() {
914        let addr: usize = 0x0080_0000_1000; // 64B-aligned
915        let start: u8 = 0;
916        let rows: u8 = 15;
917        let xs: u64 = ((start as u64 & 0x3F) << 53) | (addr as u64) | (rows as u64 & 0xF);
918        // START field (bits 58:53) = 0
919        assert_eq!((xs >> 53) & 0x3F, 0);
920        // bit 52 = 0 (TensorLoad, not TensorLoadB)
921        assert_eq!((xs >> 52) & 1, 0);
922        // ROWS = 15
923        assert_eq!(xs & 0xF, 15);
924        // ADDR embedded at bits 47:6 (addr = 0x80_0000_1000, bits fit in 47:6)
925        let addr_bits = addr as u64 & 0x0000_FFFF_FFFF_FFFF;
926        assert_eq!(xs & addr_bits, addr_bits);
927    }
928
929    /// Verify that fma16a32_xs differs from fma32_xs only in bits 3:1.
930    #[test]
931    fn fma16a32_xs_tensortype() {
932        let xs32 = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
933        let xs16 = fma16a32_xs(3, 15, 15, 0, true, 0, 0, false, false);
934        // bits 3:1 must be 001 (value 2) for FMA16A32
935        assert_eq!((xs16 >> 1) & 0x7, 1);
936        // all other bits identical
937        assert_eq!(xs32 & !(0x7 << 1), xs16 & !(0x7 << 1));
938    }
939
940    /// Verify ima8a32_xs bits 3:1 = 011 and the DST/UA/UB fields.
941    #[test]
942    fn ima8a32_xs_fields() {
943        let xs = ima8a32_xs(
944            /*bcols*/ 3, /*arows*/ 15, /*acols*/ 15, /*aoffset*/ 0,
945            /*b_in_mem*/ false, /*bstart*/ 0, /*astart*/ 0, /*dst_fp*/ true,
946            /*b_unsigned*/ true, /*a_unsigned*/ true, /*mul_only*/ false,
947            /*use_mask*/ false,
948        );
949        // TensorType bits 3:1 = 011
950        assert_eq!((xs >> 1) & 0x7, 3);
951        // DST = 1 at bit 23
952        assert_eq!((xs >> 23) & 1, 1);
953        // UB = 1 at bit 22
954        assert_eq!((xs >> 22) & 1, 1);
955        // UA = 1 at bit 21
956        assert_eq!((xs >> 21) & 1, 1);
957        // TENB = 0 (b_in_mem = false)
958        assert_eq!((xs >> 20) & 1, 0);
959        // BCOLS, AROWS, ACOLS
960        assert_eq!((xs >> 55) & 0x3, 3);
961        assert_eq!((xs >> 51) & 0xF, 15);
962        assert_eq!((xs >> 47) & 0xF, 15);
963    }
964
965    /// Verify ima8a32_xs with b_in_mem=true sets TENB bit.
966    #[test]
967    fn ima8a32_xs_b_in_mem() {
968        let xs = ima8a32_xs(0, 0, 0, 0, true, 0, 0, false, false, false, false, false);
969        assert_eq!((xs >> 20) & 1, 1); // TENB = 1 (memory path)
970    }
971
972    /// Verify tensor_store_from_scp xs: bit 48 = 1, STEP, START, ROWS, ADDR.
973    #[test]
974    fn store_from_scp_xs_fields() {
975        let addr: usize = 0x0080_0000_2000; // 64B-aligned
976        let xs: u64 = (((4_u64 - 1) & 0x3) << 62)  // step=4 -> STEP=3
977                   |  ((12_u64 & 0x3F) << 56)        // start=12
978                   |  ((7_u64  & 0xF)  << 51)        // rows=7
979                   |  (1_u64           << 48)         // source = scratchpad
980                   |  (addr as u64 & 0x0000_FFFF_FFFF_FFC0_usize as u64);
981        // bit 48 = 1 (TensorStoreFromScp discriminator)
982        assert_eq!((xs >> 48) & 1, 1);
983        // STEP = 3 (step - 1) at bits 63:62
984        assert_eq!(xs >> 62, 3);
985        // START = 12 at bits 61:56
986        assert_eq!((xs >> 56) & 0x3F, 12);
987        // ROWS = 7 at bits 54:51
988        assert_eq!((xs >> 51) & 0xF, 7);
989        // ADDR embedded at bits 47:6 (addr is 64B-aligned)
990        assert_eq!(xs & addr as u64, addr as u64);
991    }
992
993    /// Verify tensor_send xs: bits 1:0 = 00, FREG, COUNT, TARGET fields.
994    #[test]
995    fn tensor_send_xs_fields() {
996        let freg: u8 = 16;
997        let count: u8 = 8;
998        let target: u16 = 5;
999        let xs: u64 = ((freg as u64 & 0x1F) << 57)
1000            | ((count as u64 & 0x7F) << 16)
1001            | ((target as u64 & 0x1FFF) << 3);
1002        // bits 1:0 = 00 (TensorSend)
1003        assert_eq!(xs & 0x3, 0);
1004        // FREG at bits 61:57
1005        assert_eq!((xs >> 57) & 0x1F, 16);
1006        // COUNT at bits 22:16
1007        assert_eq!((xs >> 16) & 0x7F, 8);
1008        // TARGET at bits 15:3
1009        assert_eq!((xs >> 3) & 0x1FFF, 5);
1010    }
1011
1012    /// Verify tensor_recv xs: bits 1:0 = 01, FUNCT field.
1013    #[test]
1014    fn tensor_recv_xs_fields() {
1015        let xs: u64 = ((4_u64 & 0x1F)   << 57)   // freg=4
1016                   |  ((ReduceFunct::Fadd as u64 & 0xF) << 24)  // FUNCT=0 (FADD)
1017                   |  ((16_u64 & 0x7F)  << 16)   // count=16
1018                   |  ((3_u64 & 0x1FFF) << 3)    // source=3
1019                   |  1_u64; // bits 1:0 = 01 (TensorRecv)
1020        // bits 1:0 = 01
1021        assert_eq!(xs & 0x3, 1);
1022        // FUNCT = 0 (FADD) at bits 27:24
1023        assert_eq!((xs >> 24) & 0xF, 0);
1024        // FREG at bits 61:57
1025        assert_eq!((xs >> 57) & 0x1F, 4);
1026    }
1027
1028    /// Verify TensorStore xs encoding: STEP=0, FREG=0, SIZE=3.
1029    #[test]
1030    fn tensor_store_xs_fields() {
1031        let addr: usize = 0x0080_0000_2000; // 64B-aligned
1032        let arows: u8 = 7;
1033        let xs: u64 = (3_u64 << 55)              // SIZE=3
1034                   |  ((arows as u64) << 51)
1035                   |  (addr as u64 & !0xF_usize as u64);
1036        // SIZE = 3 at bits 56:55
1037        assert_eq!((xs >> 55) & 0x3, 3);
1038        // ROWS = 7 at bits 54:51
1039        assert_eq!((xs >> 51) & 0xF, 7);
1040        // STEP = 0 at bits 63:62
1041        assert_eq!(xs >> 62, 0);
1042        // FREG = 0 at bits 61:57
1043        assert_eq!((xs >> 57) & 0x1F, 0);
1044        // ADDR is embedded (addr is 64B-aligned, so !0xF == addr)
1045        assert_eq!(xs & (addr as u64), addr as u64);
1046    }
1047}