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}