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}