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}