1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
//! Tensor-extension intrinsics for the ET-SoC-1 Minion core.
//!
//! All tensor instructions on the ET-SoC-1 are encoded as standard RISC-V
//! `csrrw xd, <csr>, xs` writes (see PRM Chapter 9). No custom opcode or
//! target-feature extension is required: `riscv64gc` suffices because the
//! operand registers are ordinary integer GPRs (the source value `xs` is an
//! integer register; the FP register file is accessed implicitly by the
//! tensor co-processor hardware, not by the instruction encoding).
//!
//! # Concurrency model
//!
//! The tensor co-processor operates independently of the RISC-V hart's
//! integer pipeline. Issuing a tensor instruction initiates an asynchronous
//! operation; the hart must call [`tensor_wait`] with the appropriate
//! [`TensorEvent`] before reading results or reusing the scratchpad. The
//! ordering guarantees are:
//!
//! - `TensorWait(Load0)` before `tensor_fma32` / `tensor_fma16a32` /
//! `tensor_ima8a32`: scratchpad A (and B when TENB=0) is populated.
//! - `TensorWait(Fma)` before `tensor_store` / `tensor_store_from_scp`:
//! FP register file (or TenC for IMA8A32 with DST=0) holds final C.
//! - `TensorWait(Store)` drains only tensor store DMA; prefer it over a full
//! `fence rw, rw` when only tensor-store ordering is required.
//! - `TensorWait(LoadL2_0)` or `TensorWait(LoadL2_1)` after
//! [`tensor_load_l2`]: the shire L2 prefetch has completed.
//! - `TensorWait(CacheOp)` after `cache_writeback` / `cache_invalidate` /
//! `cache_flush`: all L1 cache management operations have completed.
//! - `fence rw, rw` (via [`crate::fence`]) after the final store: writes are
//! visible to other Minions and the DMA engine before the kernel returns.
//!
//! # Scratchpad layout
//!
//! Each Minion has a private 48-line L1 scratchpad (3 072 bytes). Only the
//! primary hart of the Minion (hart 0, i.e. `mhartid & 1 == 0`) should issue
//! tensor load/store/FMA instructions; the companion hart (hart 1) must not
//! touch the same scratchpad lines concurrently.
use asm;
// ---------------------------------------------------------------------------
// CSR addresses (PRM Chapter 9, Table 9-1)
// ---------------------------------------------------------------------------
/// TensorFMA CSR (`tensor_fma`): selects the FMA variant via xs bits 3:1.
/// (PRM Table 9-7: TensorFMA32 = 3:1 000, TensorFMA16A32 = 001, ...)
pub const CSR_TENSOR_FMA: u16 = 0x801;
/// TensorWait CSR (`tensor_wait`): stalls the hart until the requested event.
pub const CSR_TENSOR_WAIT: u16 = 0x830;
/// TensorError CSR (`tensor_error`): latched error flags from the co-processor.
/// (PRM Table 9-1: 0x808, not 0x831)
pub const CSR_TENSOR_ERROR: u16 = 0x808;
/// TensorMask CSR (`tensor_mask`): per-row enable bits for the A tile.
/// (PRM Table 9-1: 0x805, not 0x832)
pub const CSR_TENSOR_MASK: u16 = 0x805;
/// TensorStore CSR (`tensor_store`): store from FP registers (bit 48 = 0) or
/// from the L1 scratchpad (bit 48 = 1 = TensorStoreFromScp) to memory.
/// (PRM Table 9-7: 0x87F, not 0x83E)
pub const CSR_TENSOR_STORE: u16 = 0x87F;
/// TensorLoad / TensorLoadB CSR (`tensor_load`): load from memory to the L1
/// scratchpad (xs bit 52 = 0) or to the TenB register file (bit 52 = 1).
pub const CSR_TENSOR_LOAD: u16 = 0x83F;
/// TensorLoadL2Scp CSR: loads rows from memory to the shire L2 cache without
/// consuming any L1 scratchpad lines. Useful for prefetching A strips while
/// the current k-loop tile executes, so the subsequent `tensor_load` (L1 fill)
/// completes from L2 rather than DRAM.
pub const CSR_TENSOR_LOAD_L2: u16 = 0x85F;
/// TensorReduce CSR (`tensor_reduce`): hart-to-hart register-file exchange.
/// xs bits 1:0 select the variant: TensorSend=00, TensorRecv=01,
/// TensorBroadcast=10, TensorReduce=11. (PRM Table 9-7: 0x800)
pub const CSR_TENSOR_REDUCE: u16 = 0x800;
// ---------------------------------------------------------------------------
// TensorWait event codes (PRM Table 9-2, xs bits 3:0)
// ---------------------------------------------------------------------------
/// Tensor co-processor synchronisation events for [`tensor_wait`].
///
/// The four-bit EVENT field in the TensorWait `xs` register selects which
/// outstanding operation the hart waits for before the instruction retires.
/// (PRM Table 9-2.)
///
/// This enum is `#[non_exhaustive]`: match arms outside this crate must
/// include a wildcard arm.
// ---------------------------------------------------------------------------
// TensorError (PRM Table 9-3)
// ---------------------------------------------------------------------------
/// Tensor co-processor error status, returned by [`check_tensor_error`].
///
/// The raw value is the 64-bit content of the `tensor_error` CSR (0x808).
/// Named bit accessors will be added once PRM Table 9-3 bit positions are
/// confirmed on hardware. Use [`raw`](TensorError::raw) to inspect the value
/// directly in the interim.
;
// ---------------------------------------------------------------------------
// Public intrinsic functions
// ---------------------------------------------------------------------------
/// Stall the hart until the specified tensor co-processor event fires.
///
/// This must be called between dependent tensor operations to enforce ordering
/// -- the co-processor and the hart pipeline are otherwise decoupled.
/// Read the tensor co-processor error status register.
///
/// Returns 0 when no error has occurred since the last reset. A non-zero
/// value encodes the error class in bits defined by PRM Table 9-3. Call
/// after `tensor_wait` to check for co-processor faults. Prefer
/// [`check_tensor_error`] to obtain a typed result.
/// Check the tensor co-processor error register and return a typed result.
///
/// Returns `Ok(())` when no fault has been latched. Returns `Err(TensorError)`
/// containing the raw CSR value otherwise. Call after `tensor_wait` to verify
/// that the preceding tensor operation completed without fault. Named bit
/// accessors on [`TensorError`] will be added once PRM Table 9-3 bit positions
/// are confirmed on hardware.
///
/// # Example
/// ```no_run
/// # use et_kernel::tensor::{TensorEvent, tensor_wait, check_tensor_error};
/// # unsafe {
/// tensor_wait(TensorEvent::Fma);
/// check_tensor_error().expect("TensorFMA fault");
/// # }
/// ```
/// Initiate an asynchronous TensorLoadL2Scp from memory into the shire L2 cache.
///
/// Identical to [`tensor_load`] in xs encoding and x31 convention, but targets
/// CSR `0x85F` (TensorLoadL2Scp) rather than `0x83F`. The rows are loaded into
/// the shire L2 without consuming any L1 scratchpad lines. Use this to prefetch
/// A strips while the current k-loop FMA executes; the subsequent
/// [`tensor_load`] for the same address will then complete from L2 rather than
/// DRAM, removing A-DMA latency from the FMA critical path.
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first row in memory.
/// - `start`: L2 target line index.
/// - `rows`: rows to load minus one (0..=15).
/// - `id`: selects the wait event (false = `LoadL2_0`, true = `LoadL2_1`).
/// Use `LoadL2_1` when a [`tensor_load`] with `id: false` is also in flight.
/// - `stride`: row stride in bytes (64-byte aligned); placed in x31.
///
/// Call `tensor_wait(TensorEvent::LoadL2_0)` (or `LoadL2_1` if `id = true`)
/// before the scratchpad fill from the same address. Do not use `CacheOp`
/// (event 6) -- TensorLoadL2Scp requires events 2/3 per PRM Table 9-2.
///
/// # Safety
/// Same constraints as [`tensor_load`]: `addr` must be aligned and within
/// device memory; must be called from the primary hart.
pub unsafe
/// Write the per-row enable mask for the next TensorFMA.
///
/// Bit `i` in `mask` enables row `i` of the A tile. Setting bit `i = 0`
/// suppresses the update to C row `i` (useful for partial M tiles when the
/// mask register is more convenient than setting AROWS). For most uses,
/// leave the mask at its reset value of all-ones and control the tile size
/// via the AROWS field in [`tensor_fma32`].
/// Initiate an asynchronous TensorLoad from memory into the L1 scratchpad.
///
/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into
/// L1 scratchpad lines `start` through `start + rows`. Row `i` is read from
/// address `addr + i * stride`. The operation is asynchronous: call
/// `tensor_wait(TensorEvent::Load0)` (or `Load1` if `id = true`) before
/// reading the scratchpad in a subsequent [`tensor_fma32`].
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first row in memory.
/// - `start`: L1 scratchpad starting line index (0..=47).
/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
/// Loads `rows + 1` cache lines.
/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
/// - `stride`: row stride in bytes (64-byte aligned); placed in x31 by this
/// function immediately before the CSRRW instruction.
///
/// # Safety
/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` valid,
/// readable bytes of device memory.
/// - Must be called from the primary hart of the Minion (mhartid & 1 == 0).
pub unsafe
/// Initiate an asynchronous TensorLoadInterleave16 from memory into the L1 scratchpad.
///
/// Identical to [`tensor_load`] except that the hardware automatically
/// interleaves consecutive fp16 row pairs during the DMA transfer, producing
/// the 2-row-interleaved layout that [`tensor_fma16a32`] expects in the
/// scratchpad. This avoids a host-side pre-packing pass for A tiles when the
/// source data is plain row-major fp16 in DRAM.
///
/// The distinction from [`tensor_load_b`]: TensorLoadInterleave16 writes to
/// the L1 scratchpad (bit 52 = 0) and is suitable for A tiles passed to
/// [`tensor_fma16a32`] with `tenb = false`. The TenB register-file path has
/// no hardware interleave mode; B must be pre-packed host-side.
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first row in memory.
/// - `start`: L1 scratchpad starting line index (0..=47).
/// - `rows`: number of rows to load minus one (ROWS field, 0..=15).
/// Loads `rows + 1` cache lines.
/// - `id`: selects the TensorWait event (false = `Load0`, true = `Load1`).
/// - `stride`: row stride in bytes (64-byte aligned); placed in x31.
///
/// # Safety
/// Same alignment and primary-hart constraints as [`tensor_load`].
pub unsafe
/// Initiate an asynchronous TensorLoadB from memory into the TenB register file.
///
/// Loads `rows + 1` consecutive rows of 64 bytes each from memory into the
/// dedicated TenB buffer. This forward-pairs with the next [`tensor_fma32`]
/// call that uses `tenb = true`; the FMA waits internally for the load to
/// complete, so no explicit `tensor_wait` is needed between LoadB and FMA.
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first B row in memory.
/// - `rows`: B rows to load minus one (ACOLS of the subsequent FMA, 0..=15).
/// - `coop`: set for cooperative multi-hart loading (advanced; leave false).
/// - `stride`: row stride of B in bytes (64-byte aligned); placed in x31.
/// - `id`: load event identifier placed in bit 0 of x31 (false = `Load0`,
/// true = `Load1`). Use `Load1` when a `tensor_load` with `id: false` is
/// also in flight, so that `tensor_wait(Load0)` waits only for the A tile
/// and not for the B DMA (which forward-pairs with the FMA anyway).
///
/// # Note: TenB path has no hardware interleave variant
///
/// The TenB register-file path (xs bit 52 = 1) does not support hardware
/// interleaving of consecutive fp16 rows. B must be pre-packed host-side into
/// the 2-row-interleaved layout that FMA16A32 expects before upload.
/// [`tensor_load_interleave16`] (xs bits 61:59 = 010, xs bit 52 = 0)
/// interleaves from plain row-major fp16 in DRAM into the L1 scratchpad, but
/// it targets the scratchpad path only, not the TenB register file; there is
/// no interleave variant for the TenB path.
///
/// # Safety
/// Same alignment and primary-hart constraints as [`tensor_load`].
pub unsafe
/// Build the xs value for a TensorFMA32 instruction.
///
/// The FMA computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when
/// `mul_only = true`), accumulating into the FP register file.
///
/// # Parameters
/// - `bcols`: B column groups minus one (BCOLS field, 0..=3; output columns
/// = 4*(bcols+1), e.g. 3 -> 16 columns).
/// - `arows`: A tile rows minus one (AROWS field, 0..=15).
/// - `acols`: A tile columns minus one (ACOLS field, 0..=15); also the
/// number of B rows loaded by the preceding [`tensor_load_b`].
/// - `aoffset`: byte offset within each scratchpad line where A row data
/// begins, in 4-byte units (AOFFSET, 0..=15). Use 0 when A columns start
/// at the beginning of a cache line.
/// - `tenb`: `true` to read B from the TenB register file (filled by the
/// preceding [`tensor_load_b`]); `false` to read from the L1 scratchpad
/// at `bstart`.
/// - `bstart`: scratchpad line index of B (ignored when `tenb = true`).
/// - `astart`: scratchpad line index of A (ASTART field, 0..=47).
/// - `mul_only`: `true` for C = A*B (ignore existing FP register values);
/// `false` for C += A*B (accumulate into current FP registers).
/// - `use_mask`: apply the tensor_mask row-enable register.
/// Initiate an asynchronous TensorFMA32.
///
/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma32_xs`]. The
/// operation is asynchronous: call `tensor_wait(TensorEvent::Fma)` before
/// reading the FP register file or issuing a subsequent [`tensor_store`].
///
/// # Safety
/// - The L1 scratchpad must be fully populated (TensorLoad with subsequent
/// `tensor_wait(Load0)`) before this call when `tenb = false`, or
/// equivalently [`tensor_load_b`] must have been issued before this call
/// for the TenB path.
/// - Must be called from the primary hart of the Minion.
pub unsafe
/// Build the xs value for a TensorFMA16A32 instruction.
///
/// Computes C += A * B with fp16 inputs and fp32 accumulation. The hardware
/// processes two K-columns per clock in a fused 3-way addition that is not
/// IEEE754-equivalent to two separate adds: the internal partial sum is
/// unrounded, and the final result is rounded toward zero (RTZ), not to
/// nearest. This introduces a systematic truncation bias. Measured RMS
/// relative error is approximately 2.6e-4 against an fp32 reference, flat
/// between K=2048 and K=4096; input-rounding dominates the RTZ bias at those
/// depths. The xs bit layout is identical to [`fma32_xs`] except bits 3:1 =
/// `001` (FMA16A32 TensorType selector).
///
/// # Tile geometry -- ACOLS differs from FMA32
///
/// The ACOLS field contracts a different number of K elements than in
/// [`fma32_xs`]:
///
/// - `acols = n` means K = 2*(n+1) fp16 pairs (two K-columns per step).
/// For example, `acols=0` -> K=2, `acols=15` -> K=32.
/// - Each A row in the L1 scratchpad occupies `(acols+1)*4` bytes,
/// holding `(acols+1)*2` fp16 values (two per group of ACOLS+1 groups).
/// - The paired [`tensor_load_b`] must be issued with `rows = acols`; the
/// hardware fires `tensor_error[6]` if `LoadB.ROWS != ACOLS`.
///
/// # Parameters
/// (same names as [`fma32_xs`]; `tenb = true` selects the TenB register file
/// for B.)
/// Initiate an asynchronous TensorFMA16A32.
///
/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`fma16a32_xs`].
/// The hardware selects the FMA16A32 path via bits 3:1 = `001` in xs.
/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
///
/// # Safety
/// Same constraints as [`tensor_fma32`].
pub unsafe
/// Build the xs value for a TensorIMA8A32 instruction.
///
/// Computes C[i][j] += A[i][k] * B[k][j] (or C = A*B when `mul_only = true`),
/// where A and B hold 8-bit integer elements and C accumulates as 32-bit signed
/// integers. The A matrix is `(AROWS+1) x (ACOLS+1)*4` int8 elements; the B
/// matrix is `(ACOLS+1)*4 x (BCOLS+1)*16` int8 elements (interleaved 4 columns
/// at a time); the output is `(AROWS+1) x (BCOLS+1)*4` int32 values.
///
/// # Parameters
/// - `bcols`: B column groups minus one (BCOLS, 0..=3; output columns = 4*(bcols+1)).
/// - `arows`: A tile rows minus one (AROWS, 0..=15).
/// - `acols`: A tile column groups minus one (ACOLS, 0..=15). Each group
/// contains 4 int8 K-elements, so `acols = n` contracts K = 4*(n+1) rows
/// (e.g. `acols=0` -> K=4, `acols=15` -> K=64). Each A row in the L1
/// scratchpad occupies `(acols+1)*4` bytes.
/// - `aoffset`: Byte offset within each scratchpad line for A data, in 4-byte units
/// (AOFFSET, 0..=15).
/// - `b_in_mem`: `true` if B is transferred via the memory DMA path; `false` for L1
/// scratchpad. (TENB = 1 means memory for IMA8A32, unlike FMA where TENB=1 is TenB
/// register file.)
/// - `bstart`: Starting scratchpad line for B; ignored when `b_in_mem = true`.
/// - `astart`: Starting scratchpad line for A (ASTART, 0..=47).
/// - `dst_fp`: `true` to write the int32 result to the FP register file;
/// `false` to write to the TenC register file. (DST, xs bit 23)
/// - `b_unsigned`: `true` if B elements are unsigned; `false` for signed.
/// - `a_unsigned`: `true` if A elements are unsigned; `false` for signed.
/// - `mul_only`: `true` for C = A*B; `false` for C += A*B.
/// - `use_mask`: Apply the tensor_mask row-enable register.
/// Initiate an asynchronous TensorIMA8A32.
///
/// Issues `csrrw x0, 0x801, xs` where `xs` is built by [`ima8a32_xs`].
/// The hardware selects the integer-GEMM path via bits 3:1 = `011` in xs.
/// Call `tensor_wait(TensorEvent::Fma)` before reading results.
///
/// # Safety
/// Same constraints as [`tensor_fma32`].
pub unsafe
/// Initiate an asynchronous TensorStoreFromScp to memory from the L1 scratchpad.
///
/// Stores `rows + 1` 64-byte scratchpad lines to memory, bypassing the L1 data
/// cache and L2 cache. Consecutive scratchpad lines are spaced `step` lines apart
/// (so `step = 1` stores consecutive lines); consecutive destination rows are
/// spaced `stride` bytes apart.
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first destination row.
/// - `rows`: Number of rows to store minus one (ROWS, 0..=15).
/// - `start`: Starting L1 scratchpad cache line (0..=47).
/// - `step`: Scratchpad line stride (1..=4); encoded as STEP = step - 1.
/// - `stride`: Destination row stride in bytes; placed in x31. Low 6 bits are
/// ignored by the hardware (rows are 64-byte aligned in memory).
///
/// # Safety
/// - `addr` must be 64-byte aligned and point to `(rows + 1) * stride` bytes of
/// writable device memory.
/// - Must be called from the primary hart of the Minion.
pub unsafe
/// Reduction function selector for [`tensor_recv`].
///
/// Specifies how the received values are combined with the values already held
/// in the destination FP registers. (PRM Table 9-8, FUNCT field.)
/// Initiate an asynchronous TensorSend.
///
/// Pushes `count` consecutive FP registers starting at `freg` from this hart
/// to hart 0 of the Minion identified by `target`. The partner hart must issue
/// a matching [`tensor_recv`]. This is the low-level primitive for hart-to-hart
/// reduction without software memory traffic.
///
/// # Parameters
/// - `freg`: Starting FP register index (0..=31).
/// - `count`: Number of FP registers to send (COUNT field, 0..=127).
/// - `target`: Destination Minion ID (TARGET field, bits 15:3 of xs).
///
/// # Safety
/// - The partner hart must call [`tensor_recv`] with the matching `source` and
/// `count` before the send retires.
/// - Must be called from the primary hart of the Minion.
pub unsafe
/// Initiate an asynchronous TensorRecv.
///
/// Receives `count` FP registers from the Minion identified by `source` and
/// combines them with the local FP registers starting at `freg` using the
/// operation specified by `funct`. This is the matching receive primitive for
/// [`tensor_send`].
///
/// # Parameters
/// - `freg`: Starting local FP register index (0..=31).
/// - `funct`: Combination operation applied to received and local values.
/// - `count`: Number of FP registers to receive (0..=127); must match the
/// sender's `count`.
/// - `source`: Source Minion ID (SOURCE field, bits 15:3 of xs).
///
/// # Safety
/// - The partner hart must have called [`tensor_send`] before this retires.
/// - Must be called from the primary hart of the Minion.
pub unsafe
/// Initiate an asynchronous TensorStore from the FP register file to memory.
///
/// Stores `arows + 1` rows of 64 bytes each (16 f32 per row, occupying two
/// consecutive 256-bit FP registers) to memory. Row `i` is stored to
/// address `addr + i * stride`, reading from FP registers f[2i] and f[2i+1].
/// The operation is asynchronous: call [`crate::fence`] after to guarantee
/// visibility to other agents before the kernel returns.
///
/// # Parameters
/// - `addr`: 64-byte aligned virtual address of the first C row in memory.
/// - `arows`: number of C rows to store minus one (ROWS field, 0..=15).
/// - `stride`: row stride of C in bytes (64-byte aligned); placed in x31.
///
/// # Safety
/// - `addr` must be 64-byte aligned and point to `(arows + 1) * stride` bytes
/// of writable device memory.
/// - `tensor_wait(TensorEvent::Fma)` must have been called first.
/// - Must be called from the primary hart of the Minion.
pub unsafe