Skip to main content

et_kernel/
tensor.rs

1//! Tensor-extension intrinsics for the ET-SoC-1 Minion core.
2//!
3//! All tensor instructions on the ET-SoC-1 are encoded as standard RISC-V
4//! `csrrw xd, <csr>, xs` writes (see PRM Chapter 9). No custom opcode or
5//! target-feature extension is required: `riscv64imac` suffices because the
6//! operand registers are ordinary integer GPRs (the source value `xs` is an
7//! integer register; the FP register file is accessed implicitly by the
8//! tensor co-processor hardware, not by the instruction encoding).
9//!
10//! # Concurrency model
11//!
12//! The tensor co-processor operates independently of the RISC-V hart's
13//! integer pipeline. Issuing a tensor instruction initiates an asynchronous
14//! operation; the hart must call [`tensor_wait`] with the appropriate
15//! [`TensorEvent`] before reading results or reusing the scratchpad. The
16//! ordering guarantees are:
17//!
18//! - `TensorWait(Load0)` before `tensor_fma32`: scratchpad A is populated.
19//! - `TensorWait(Fma)` before `tensor_store`: FP register file holds final C.
20//! - `fence rw, rw` (via [`crate::fence`]) after `tensor_store`: stores are
21//!   visible to other Minions and the DMA engine before the kernel returns.
22//!
23//! # Scratchpad layout
24//!
25//! Each Minion has a private 48-line L1 scratchpad (3 072 bytes). Only the
26//! primary hart of the Minion (hart 0, i.e. `mhartid & 1 == 0`) should issue
27//! tensor load/store/FMA instructions; the companion hart (hart 1) must not
28//! touch the same scratchpad lines concurrently.
29
30use core::arch::asm;
31
32// ---------------------------------------------------------------------------
33// CSR addresses (PRM Chapter 9, Table 9-1)
34// ---------------------------------------------------------------------------
35
36/// TensorFMA CSR (`tensor_fma`): selects the FMA variant via xs bits 3:1.
37/// (PRM Table 9-7: TensorFMA32 = 3:1 000, TensorFMA16A32 = 001, ...)
38pub const CSR_TENSOR_FMA:   u16 = 0x801;
39/// TensorWait CSR (`tensor_wait`): stalls the hart until the requested event.
40pub const CSR_TENSOR_WAIT:  u16 = 0x830;
41/// TensorError CSR (`tensor_error`): latched error flags from the co-processor.
42/// (PRM Table 9-1: 0x808, not 0x831)
43pub const CSR_TENSOR_ERROR: u16 = 0x808;
44/// TensorMask CSR (`tensor_mask`): per-row enable bits for the A tile.
45/// (PRM Table 9-1: 0x805, not 0x832)
46pub const CSR_TENSOR_MASK:  u16 = 0x805;
47/// TensorStore CSR (`tensor_store`): store from FP registers (bit 48 = 0) or
48/// from the L1 scratchpad (bit 48 = 1 = TensorStoreFromScp) to memory.
49/// (PRM Table 9-7: 0x87F, not 0x83E)
50pub const CSR_TENSOR_STORE: u16 = 0x87F;
51/// TensorLoad / TensorLoadB CSR (`tensor_load`): load from memory to the L1
52/// scratchpad (xs bit 52 = 0) or to the TenB register file (bit 52 = 1).
53pub const CSR_TENSOR_LOAD:  u16 = 0x83F;
54
55// ---------------------------------------------------------------------------
56// TensorWait event codes (PRM Table 9-2, xs bits 3:0)
57// ---------------------------------------------------------------------------
58
59/// Tensor co-processor synchronisation events for [`tensor_wait`].
60///
61/// The four-bit EVENT field in the TensorWait `xs` register selects which
62/// outstanding operation the hart waits for before the instruction retires.
63#[repr(u8)]
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum TensorEvent {
66    /// Completion of all TensorLoad operations issued with ID = 0.
67    Load0 = 0,
68    /// Completion of all TensorLoad operations issued with ID = 1.
69    Load1 = 1,
70    /// Completion of all preceding TensorFMA operations; the FP register file
71    /// holds the final accumulated C tile and may be read or stored.
72    Fma   = 7,
73    /// Completion of all preceding TensorStore DMA transfers (PRM Table 9-2,
74    /// event code 8). Drains only the tensor store DMA, allowing the compiler
75    /// more freedom to reorder non-tensor memory accesses around it. Prefer
76    /// this over a full `fence rw, rw` when only tensor-store ordering is
77    /// required (e.g. confirming one tile is written before reusing FP registers
78    /// for the next tile in a pipelined loop).
79    Store = 8,
80}
81
82// ---------------------------------------------------------------------------
83// Public intrinsic functions
84// ---------------------------------------------------------------------------
85
86/// Stall the hart until the specified tensor co-processor event fires.
87///
88/// This must be called between dependent tensor operations to enforce ordering
89/// -- the co-processor and the hart pipeline are otherwise decoupled.
90#[inline(always)]
91pub fn tensor_wait(event: TensorEvent) {
92    let xs: u64 = event as u64;
93    // SAFETY: csrrw to a U-mode-accessible tensor CSR with no memory effects
94    // from the hart's perspective; the co-processor drains its pipeline.
95    unsafe {
96        asm!(
97            concat!("csrrw x0, ", stringify!(0x830), ", {xs}"),
98            xs = in(reg) xs,
99            options(nomem, nostack, preserves_flags),
100        );
101    }
102}
103
104/// Read the tensor co-processor error status register.
105///
106/// Returns 0 when no error has occurred since the last reset. A non-zero
107/// value encodes the error class in bits defined by PRM Table 9-3. Call
108/// after `tensor_wait` to check for co-processor faults.
109#[inline(always)]
110pub fn tensor_error() -> u64 {
111    let v: u64;
112    // SAFETY: csrrs with rs1 = x0 reads without side effect.
113    unsafe {
114        asm!(
115            concat!("csrrs {v}, ", stringify!(0x808), ", x0"),
116            v = out(reg) v,
117            options(nomem, nostack, preserves_flags),
118        );
119    }
120    v
121}
122
123/// Write the per-row enable mask for the next TensorFMA.
124///
125/// Bit `i` in `mask` enables row `i` of the A tile. Setting bit `i = 0`
126/// suppresses the update to C row `i` (useful for partial M tiles when the
127/// mask register is more convenient than setting AROWS). For most uses,
128/// leave the mask at its reset value of all-ones and control the tile size
129/// via the AROWS field in [`tensor_fma32`].
130#[inline(always)]
131pub fn set_tensor_mask(mask: u16) {
132    let xs: u64 = mask as u64;
133    unsafe {
134        asm!(
135            concat!("csrrw x0, ", stringify!(0x805), ", {xs}"),
136            xs = in(reg) xs,
137            options(nomem, nostack, preserves_flags),
138        );
139    }
140}
141
142/// Initiate an asynchronous TensorLoad from memory into the L1 scratchpad.
143///
144/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into
145/// L1 scratchpad lines `start` through `start + rows`. Row `i` is read from
146/// address `addr + i * stride`. The operation is asynchronous: call
147/// `tensor_wait(TensorEvent::Load0)` (or `Load1` if `id = true`) before
148/// reading the scratchpad in a subsequent [`tensor_fma32`].
149///
150/// # Parameters
151/// - `addr`: 64-byte aligned virtual address of the first row in memory.
152/// - `start`: L1 scratchpad starting line index (0..=47).
153/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
154///   Loads `rows + 1` cache lines.
155/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
156/// - `stride`: row stride in bytes (64-byte aligned); placed in x31 by this
157///   function immediately before the CSRRW instruction.
158///
159/// # Safety
160/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` valid,
161///   readable bytes of device memory.
162/// - Must be called from the primary hart of the Minion (mhartid & 1 == 0).
163#[inline(always)]
164pub unsafe fn tensor_load(addr: usize, start: u8, rows: u8, id: bool, stride: u64) {
165    // xs bit layout (PRM Table 9-5):
166    //   63: MSK=0, 62: COOP=0, 61:59=000 (TensorLoad variant),
167    //   58:53=START (6-bit scratchpad line index),
168    //   52=0 (TensorLoad, not TensorLoadB),
169    //   51:48=0 (reserved), 47:6=ADDR>>6 (addr is 64B-aligned so bits 5:0 = 0),
170    //   5:4=0 (reserved), 3:0=ROWS.
171    let xs: u64 = ((start as u64 & 0x3F) << 53)
172               |  (addr as u64)           // bits 47:6; addr is 64B-aligned so addr & !63 == addr
173               |  (rows as u64 & 0xF);
174    // x31 (t6) carries the row stride; the hardware reads it implicitly.
175    unsafe {
176        asm!(
177            "mv t6, {stride}",
178            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
179            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
180            xs     = in(reg) xs,
181            out("t6") _,
182            options(nostack),
183        );
184    }
185}
186
187/// Initiate an asynchronous TensorLoadB from memory into the TenB register file.
188///
189/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into the
190/// dedicated TenB buffer. This forward-pairs with the next [`tensor_fma32`]
191/// call that uses `tenb = true`; the FMA waits internally for the load to
192/// complete, so no explicit `tensor_wait` is needed between LoadB and FMA.
193///
194/// # Parameters
195/// - `addr`: 64-byte aligned virtual address of the first B row in memory.
196/// - `rows`: B rows to load minus one (ACOLS of the subsequent FMA, 0..=15).
197/// - `coop`: set for cooperative multi-hart loading (advanced; leave false).
198/// - `stride`: row stride of B in bytes (64-byte aligned); placed in x31.
199/// - `id`: load event identifier placed in bit 0 of x31 (false = `Load0`,
200///   true = `Load1`). Use `Load1` when a `tensor_load` with `id: false` is
201///   also in flight, so that `tensor_wait(Load0)` waits only for the A tile
202///   and not for the B DMA (which forward-pairs with the FMA anyway).
203///
204/// # Safety
205/// Same alignment and primary-hart constraints as [`tensor_load`].
206#[inline(always)]
207pub unsafe fn tensor_load_b(addr: usize, rows: u8, coop: bool, stride: u64, id: bool) {
208    // xs bit layout (PRM Table 9-6):
209    //   63: MSK=0, 62: COOP, 61:53=0 (reserved),
210    //   52=1 (TensorLoadB distinguisher),
211    //   51:48=0 (reserved), 47:6=ADDR>>6, 5:4=0, 3:0=ROWS.
212    // x31 bit 0 = ID (identical mechanism to TensorLoad; PRM Chapter 9).
213    let xs: u64 = ((coop as u64)  << 62)
214               |  (1_u64          << 52)
215               |  (addr as u64)           // 64B-aligned: bits 47:6 correct
216               |  (rows as u64 & 0xF);
217    unsafe {
218        asm!(
219            "mv t6, {stride}",
220            concat!("csrrw x0, ", stringify!(0x83F), ", {xs}"),
221            stride = in(reg) stride | (id as u64),  // bit 0 of x31 = ID
222            xs     = in(reg) xs,
223            out("t6") _,
224            options(nostack),
225        );
226    }
227}
228
229/// Build the xs value for a TensorFMA32 instruction.
230///
231/// The FMA computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when
232/// `mul_only = true`), accumulating into the FP register file.
233///
234/// # Parameters
235/// - `bcols`:    B column groups minus one (BCOLS field, 0..=3; output columns
236///   = 4*(bcols+1), e.g. 3 -> 16 columns).
237/// - `arows`:    A tile rows minus one (AROWS field, 0..=15).
238/// - `acols`:    A tile columns minus one (ACOLS field, 0..=15); also the
239///   number of B rows loaded by the preceding [`tensor_load_b`].
240/// - `aoffset`:  byte offset within each scratchpad line where A row data
241///   begins, in 4-byte units (AOFFSET, 0..=15). Use 0 when A columns start
242///   at the beginning of a cache line.
243/// - `tenb`:     `true` to read B from the TenB register file (filled by the
244///   preceding [`tensor_load_b`]); `false` to read from the L1 scratchpad
245///   at `bstart`.
246/// - `bstart`:   scratchpad line index of B (ignored when `tenb = true`).
247/// - `astart`:   scratchpad line index of A (ASTART field, 0..=47).
248/// - `mul_only`: `true` for C = A*B (ignore existing FP register values);
249///   `false` for C += A*B (accumulate into current FP registers).
250/// - `use_mask`: apply the tensor_mask row-enable register.
251#[must_use = "the returned xs value must be passed to tensor_fma32; discarding it issues no instruction"]
252#[allow(clippy::too_many_arguments)]
253#[inline]
254pub fn fma32_xs(
255    bcols:    u8,
256    arows:    u8,
257    acols:    u8,
258    aoffset:  u8,
259    tenb:     bool,
260    bstart:   u8,
261    astart:   u8,
262    mul_only: bool,
263    use_mask: bool,
264) -> u64 {
265    // xs bit layout (PRM Table 9-4):
266    //   63: MSK, 62:57: reserved (0), 56:55: BCOLS, 54:51: AROWS,
267    //   50:47: ACOLS, 46:43: AOFFSET, 42:21: reserved (0), 20: TENB,
268    //   19:18: reserved (0), 17:12: BSTART, 11:10: reserved (0),
269    //   9:4: ASTART, 3:1: 000 (FMA32 TensorType), 0: MUL.
270    ((use_mask as u64)       << 63)
271  | ((bcols   as u64 & 0x3)  << 55)
272  | ((arows   as u64 & 0xF)  << 51)
273  | ((acols   as u64 & 0xF)  << 47)
274  | ((aoffset as u64 & 0xF)  << 43)
275  | ((tenb    as u64)        << 20)
276  | ((bstart  as u64 & 0x3F) << 12)
277  | ((astart  as u64 & 0x3F) <<  4)
278  // bits 3:1 = 000 (FMA32 TensorType selector)
279  | (mul_only as u64)
280}
281
282/// Initiate an asynchronous TensorFMA32.
283///
284/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma32_xs`]. The
285/// operation is asynchronous: call `tensor_wait(TensorEvent::Fma)` before
286/// reading the FP register file or issuing a subsequent [`tensor_store`].
287///
288/// # Safety
289/// - The L1 scratchpad must be fully populated (TensorLoad with subsequent
290///   `tensor_wait(Load0)`) before this call when `tenb = false`, or
291///   equivalently [`tensor_load_b`] must have been issued before this call
292///   for the TenB path.
293/// - Must be called from the primary hart of the Minion.
294#[inline(always)]
295pub unsafe fn tensor_fma32(xs: u64) {
296    unsafe {
297        asm!(
298            concat!("csrrw x0, ", stringify!(0x801), ", {xs}"),
299            xs = in(reg) xs,
300            options(nostack),
301        );
302    }
303}
304
305/// Initiate an asynchronous TensorStore from the FP register file to memory.
306///
307/// Stores `arows + 1` rows of 64 bytes each (16 f32 per row, occupying two
308/// consecutive 256-bit FP registers) to memory. Row `i` is stored to
309/// address `addr + i * stride`, reading from FP registers f[2i] and f[2i+1].
310/// The operation is asynchronous: call [`crate::fence`] after to guarantee
311/// visibility to other agents before the kernel returns.
312///
313/// # Parameters
314/// - `addr`:  64-byte aligned virtual address of the first C row in memory.
315/// - `arows`: number of C rows to store minus one (ROWS field, 0..=15).
316/// - `stride`: row stride of C in bytes (64-byte aligned); placed in x31.
317///
318/// # Safety
319/// - `addr` must be 64-byte aligned and point to `(arows + 1) * stride` bytes
320///   of writable device memory.
321/// - `tensor_wait(TensorEvent::Fma)` must have been called first.
322/// - Must be called from the primary hart of the Minion.
323#[inline(always)]
324pub unsafe fn tensor_store(addr: usize, arows: u8, stride: u64) {
325    // xs bit layout (PRM Table 9-7):
326    //   63:62: STEP=0 (fstep=1; row i uses f[2i] and f[2i+1]),
327    //   61:57: FREG=0 (start at f0),
328    //   56:55: SIZE=3 (64 bytes = 16 f32 per row, two 256-bit registers),
329    //   54:51: ROWS,
330    //   50:49: COOP=0 (no cooperative multi-hart store),
331    //   48:   0 (store from FP registers, not from Scp),
332    //   47:4: ADDR >> 4 (addr is 64B-aligned, so addr & !0xF == addr),
333    //   3:0:  0000.
334    // Zero fields (STEP=0 at 63:62, FREG=0 at 61:57, COOP=0 at 50:49,
335    // source=FP-registers at 48) are left as the natural zero of u64.
336    let xs: u64 = (3_u64 << 55)                        // SIZE=3 (64B/row)
337               |  ((arows as u64) << 51)               // ROWS
338               |  (addr as u64 & !0xF_usize as u64);   // ADDR[47:4]; addr is 64B-aligned
339    // x31 carries the C row stride; TensorStore uses bits [47:4] of x31.
340    unsafe {
341        asm!(
342            "mv t6, {stride}",
343            concat!("csrrw x0, ", stringify!(0x87F), ", {xs}"),
344            stride = in(reg) stride,
345            xs     = in(reg) xs,
346            out("t6") _,
347            options(nostack),
348        );
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    /// Verify the FMA32 xs bit packing for a standard full-tile configuration:
357    /// BCOLS=3, AROWS=15, ACOLS=15, AOFFSET=0, TENB=1, BSTART=0, ASTART=0.
358    #[test]
359    fn fma32_xs_full_tile() {
360        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, false, false);
361        // BCOLS=3 at bits 56:55 -> 3 << 55
362        assert_eq!(xs & (0x3 << 55), 3 << 55);
363        // AROWS=15 at bits 54:51 -> 15 << 51
364        assert_eq!(xs & (0xF << 51), 15 << 51);
365        // ACOLS=15 at bits 50:47
366        assert_eq!(xs & (0xF << 47), 15 << 47);
367        // TENB=1 at bit 20
368        assert_eq!(xs & (1 << 20), 1 << 20);
369        // MUL=0, MSK=0
370        assert_eq!(xs & 1, 0);
371        assert_eq!(xs >> 63, 0);
372    }
373
374    /// Verify mul_only sets bit 0.
375    #[test]
376    fn fma32_xs_mul_only() {
377        let xs = fma32_xs(3, 15, 15, 0, true, 0, 0, true, false);
378        assert_eq!(xs & 1, 1);
379    }
380
381    /// Verify that TensorEvent discriminants match PRM Table 9-2.
382    #[test]
383    fn tensor_event_discriminants() {
384        assert_eq!(TensorEvent::Load0 as u64, 0);
385        assert_eq!(TensorEvent::Load1 as u64, 1);
386        assert_eq!(TensorEvent::Fma   as u64, 7);
387        assert_eq!(TensorEvent::Store as u64, 8);
388    }
389
390    /// Verify TensorLoad xs encoding for addr=0x1000, start=0, rows=15.
391    #[test]
392    fn tensor_load_xs_encoding() {
393        let addr: usize = 0x0080_0000_1000; // 64B-aligned
394        let start: u8 = 0;
395        let rows: u8 = 15;
396        let xs: u64 = ((start as u64 & 0x3F) << 53)
397                   |  (addr as u64)
398                   |  (rows as u64 & 0xF);
399        // START field (bits 58:53) = 0
400        assert_eq!((xs >> 53) & 0x3F, 0);
401        // bit 52 = 0 (TensorLoad, not TensorLoadB)
402        assert_eq!((xs >> 52) & 1, 0);
403        // ROWS = 15
404        assert_eq!(xs & 0xF, 15);
405        // ADDR embedded at bits 47:6 (addr = 0x80_0000_1000, bits fit in 47:6)
406        let addr_bits = addr as u64 & 0x0000_FFFF_FFFF_FFFF;
407        assert_eq!(xs & addr_bits, addr_bits);
408    }
409
410    /// Verify TensorStore xs encoding: STEP=0, FREG=0, SIZE=3.
411    #[test]
412    fn tensor_store_xs_fields() {
413        let addr: usize = 0x0080_0000_2000; // 64B-aligned
414        let arows: u8 = 7;
415        let xs: u64 = (3_u64 << 55)              // SIZE=3
416                   |  ((arows as u64) << 51)
417                   |  (addr as u64 & !0xF_usize as u64);
418        // SIZE = 3 at bits 56:55
419        assert_eq!((xs >> 55) & 0x3, 3);
420        // ROWS = 7 at bits 54:51
421        assert_eq!((xs >> 51) & 0xF, 7);
422        // STEP = 0 at bits 63:62
423        assert_eq!(xs >> 62, 0);
424        // FREG = 0 at bits 61:57
425        assert_eq!((xs >> 57) & 0x1F, 0);
426        // ADDR is embedded (addr is 64B-aligned, so !0xF == addr)
427        assert_eq!(xs & (addr as u64), addr as u64);
428    }
429}