et-k-rs 0.6.2

Device-side library for writing ET-SoC-1 compute kernels in pure no_std Rust
Documentation

et-k-rs

Device-side library for writing ET-SoC-1 compute kernels in pure no_std Rust — the device counterpart to the et_soc1 host crate, with no C dependency. The library (et_kernel, src/lib.rs) provides hart identity, the U-mode trace write, a hardware fence, scratchpad addressing, and the safe Grid partitioning abstraction. Launch-argument structs are shared with the host launcher through the et-abi crate, so the two sides cannot drift on layout. Three demo kernels build on the library and double as worked examples.

Kernels

  • hello-rs (src/bin/hello.rs) -- every hart writes "Hello World from hart N" to its trace buffer. A drop-in Rust replacement for the SDK's C hello.c; reimplements get_hart_id (the hartid CSR 0xCD0) and the Trace_String write directly.
  • spsc-rs (src/bin/spsc.rs) -- a single-producer/single-consumer, lock-free, non-atomic queue across two harts (plain volatile loads/stores + fence rw,rw, no atomics, no locks). A coherence probe: it showed the ET-SoC-1 is software-coherent -- fence-only cross-hart sharing does not propagate (even within one minion), so this needs explicit cache management or genuinely shared memory. See the crate root README.
  • reduce-rs (src/bin/reduce.rs) -- a data-parallel reduction (sum) over a DRAM array across a shire's 64 harts. Each hart reduces its disjoint slice (Grid::my_slice) and writes its own cache-line-padded partial cell (no false sharing); the host combines. No cross-hart sharing during the kernel, so it is coherence-clean and validated on hardware.
  • sgemm-rs (src/bin/sgemm.rs) -- single-precision GEMM (C = A*B) using the ET-SoC-1 tensor extension. Tile assignment is shire-blocked: each shire handles a contiguous slice of the tile grid, improving A-row reuse in the shire-shared L2 (+26% at N=4096 over global-cyclic). v0.1 supports alpha=1.0 and beta=0.0; N may be any positive integer (partial last-column tile handled via stride-aligned padding). Verified on hardware: 64x64x64 and 32x20x32 (partial-N) both produce correct results with zero floating-point error. See the host-side sgemm and sgemm_partial examples in et-rs/.

Tensor extension (et_kernel::tensor)

All tensor operations on the ET-SoC-1 are encoded as standard RISC-V csrrw xd, <csr>, xs writes (PRM Chapter 9). No custom opcode or target feature is required; riscv64gc suffices. The tensor module exposes typed, inline-asm wrappers for each instruction:

Function CSR Role
tensor_load 0x83F Async load from DRAM into L1 scratchpad (ID=0 or ID=1).
tensor_load_b 0x83F Async load from DRAM into the TenB register file (bit 52 set).
tensor_load_l2 0x85F Async prefetch from DRAM to shire L2 cache (no L1 fill).
tensor_fma32 0x801 Async FMA32: C += A * B (or C = A * B when mul_only).
tensor_store 0x87F Async store from FP register file to DRAM.
tensor_wait 0x830 Stall hart until Load0, Load1, Fma, or Store event fires.
tensor_error 0x808 Read latched co-processor error flags (#[must_use]).
check_tensor_error 0x808 Returns Ok(()) or Err(TensorError) (typed wrapper).
set_tensor_mask 0x805 Write per-row FMA enable bits.

fma32_xs constructs the xs bit field for tensor_fma32, encoding BCOLS, AROWS, ACOLS, AOFFSET, TENB, BSTART, ASTART, MUL, and MSK from PRM Table 9-4.

x31 (t6) carries the row stride for tensor_load, tensor_load_b, and tensor_store; each function sets it atomically with the CSRRW inside the same asm block.

Usage pattern

use et_kernel::tensor::{
    TensorEvent, check_tensor_error, fma32_xs,
    tensor_fma32, tensor_load, tensor_load_b, tensor_store, tensor_wait,
};
use et_kernel::fence;

// Load A tile into L1 scratchpad (lines 0..arows); ID=false -> Load0.
unsafe { tensor_load(a_addr, 0, arows, /*id=*/false, lda); }
unsafe { tensor_wait(TensorEvent::Load0); }

// Load B tile into TenB register file; ID=true -> Load1, keeping B's
// event independent of the A Load0 above.
unsafe { tensor_load_b(b_addr, acols, /*coop=*/false, ldb, /*id=*/true); }

// Issue FMA: C = A * B (first k-tile) or C += A * B (subsequent).
let xs = fma32_xs(bcols, arows, acols, 0, true, 0, 0, k_tile == 0, false);
unsafe { tensor_fma32(xs); tensor_wait(TensorEvent::Fma); }

// Optional: check for co-processor faults before storing.
check_tensor_error().expect("tensor co-processor fault");

// Store C from FP registers to DRAM, then drain the store and fence.
unsafe { tensor_store(c_addr, arows, ldc); }
unsafe { tensor_wait(TensorEvent::Store); }
fence();

PMU counters (et_kernel::pmu)

U-mode-accessible hardware performance counters for characterising kernel behaviour. Reads hpmcounterN (CSR 0xC03 + (N-3)) without any privilege escalation.

RTLMIN-6496 workaround. All read functions emit four consecutive csrrs instructions for the same CSR in a 16-byte-aligned block (.align 4) and return the fourth value. This satisfies the hardware erratum requirement; a single read returns an unreliable value on affected silicon. The timestamp() function in the library root applies the same workaround.

use et_kernel::pmu::{PmuEvent, pmu_read};

// Read counter 4 before and after the k-loop; the delta is the number of
// TFMA_WAIT_TENB stall cycles (assuming firmware assigned PmuEvent::TfmaWaitTenb
// to counter 4 via mhpmevent4).
let before = pmu_read(4);
// ... tensor k-loop ...
let after  = pmu_read(4);
let stalls = after.wrapping_sub(before);

Counter availability. Only hpmcounter3-hpmcounter8 accumulate events; counters 9-31 are tied to zero by the hardware. mcycle (CSR 0xC00) and minstret (CSR 0xC02) are also permanently zero -- use PmuEvent::Cycles in hpmcounter3-hpmcounter6 and PmuEvent::RetiredInst0/1 instead (PRM section 1.3.2).

Item Description
pmu_read(counter: u8) -> u64 Read hpmcounterN for N in 3..=8; counters 9-31 return 0 (RTLMIN-6496 workaround applied).
pmu_read_cycle() -> u64 Always returns 0 (mcycle is tied to zero on ET-SoC-1).
pmu_read_instret() -> u64 Always returns 0 (minstret is tied to zero on ET-SoC-1).
PmuEvent 29 Minion-level event codes for mhpmevent3-mhpmevent6 (PRM section 1.3.2, Table 1-3).
NeighborhoodEvent 22 neighbourhood-level event codes for mhpmevent7-mhpmevent8 (PRM section 1.3.2, Table 1-4).

PS SIMD (et_kernel::simd)

The simd module provides wrappers for the ET-SoC-1 packed-single (PS) SIMD extension, gated on cfg(target_feature = "f"). Encodings are sourced from esperanto-opc.h in the ET-SoC-1 binutils fork (present on aifoundry3 at /home/rich/riscv-gnu-toolchain/gdb/include/opcode/esperanto-opc.h).

Item PS instruction Notes
broadcast_ps(scalar, dest) FBCX.PS f{dest}, tmp Broadcasts scalar to all 8 PS lanes of register dest (0..=31). fmv.x.w moves the bit pattern to a GPR first.
fmul_ps_row(row, scratch) FMUL.PS x 2 Element-wise multiplies f[2*row] and f[2*row+1] by pre-broadcast register f[scratch]. Call broadcast_ps first.
scale_c_row(row, alpha, scratch) FBCX.PS + FMUL.PS x 2 Convenience wrapper: broadcast then scale. Equivalent to broadcast_ps(alpha, scratch) + fmul_ps_row(row, scratch).
PS_SCRATCH_DEFAULT -- 28 (f28/ft8). Safe for C tiles with at most 14 rows.

Scratch register. broadcast_ps clobbers f[dest]; choose dest so it does not hold live C-tile data for the row being scaled (i.e. dest != 2*row and dest != 2*row+1). For tiles of at most 14 rows, pass PS_SCRATCH_DEFAULT (28). For a full 16-row tile, rows 14 (f28/f29) and 15 (f30/f31) conflict with f28; the caller must spill one free FP register to the stack, broadcast alpha into it, scale the conflicting row, then restore. See the simd module doc for the recommended pattern.

Hardware-verified on aifoundry3 (2026-09-18): all 1024 Minions produced correct results for FBCX.PS and FMUL.PS. Requires target-feature=+f (included automatically with the riscv64gc target).

The safety story (reduce-rs)

The kernel body is safe Rust over Grid: a hart can obtain only its own input slice and its own output cell, so an out-of-partition access or a cross-hart data race is unrepresentable. The only unsafe is a thin, commented boundary that turns launch arguments and device addresses into typed slices.

Build

Cross-compiles to the compute harts (RV64GC); target, code model (medium = medany, for the fixed high link address) and linker script are in .cargo/config.toml:

rustup target add riscv64gc-unknown-none-elf   # once
cargo build --release
# -> target/riscv64gc-unknown-none-elf/release/{hello-rs,spsc-rs,reduce-rs}

The library (--lib) also compiles on the host target for IDE type-checking and rust-analyzer support. All items that contain RISC-V inline assembly (hart_id, fence, timestamp, trace_str, kernel_entry!, Grid, and the tensor/pmu/cache modules) are gated on #[cfg(target_arch = "riscv64")] and are absent on non-RISC-V targets. MsgBuf, device_slice, scp_shire_base, and CACHE_LINE remain available everywhere. The binary kernels (hello-rs etc.) contain device asm and still require the RISC-V target.

When consuming et-k-rs as a registry dependency from a workspace whose .cargo/config.toml sets [build] target = "riscv64gc-unknown-none-elf", build from inside the kernel crate directory so Cargo finds the config:

# From workspace root via --manifest-path: Cargo may resolve to host target.
# Safer: cd into the crate first.
cd my-kernel && cargo build --release

Run

Load and launch with the host crate's examples (from the repository root), emulator or hardware:

cd et-k-rs && cargo build --release && cd ..
K=et-k-rs/target/riscv64gc-unknown-none-elf/release
cargo run --manifest-path et-rs/Cargo.toml --release --example hello_sysemu --features emu -- $K/hello-rs   # emulator
cargo run --manifest-path et-rs/Cargo.toml --release --example reduce         -- $K/reduce-rs  # hardware
cargo run --manifest-path et-rs/Cargo.toml --release --example spsc           -- $K/spsc-rs    # hardware
cargo run --manifest-path et-rs/Cargo.toml --release --example sgemm          -- $K/sgemm-rs   # hardware; 64x64x64
cargo run --manifest-path et-rs/Cargo.toml --release --example sgemm_partial  -- $K/sgemm-rs   # hardware; 32x20x32 partial-N

Kernel facts (reference)

  • Entry/exit: _start sets gp, calls entry_point, then ecall with SYSCALL_RETURN_FROM_KERNEL (8) / KERNEL_RETURN_SUCCESS (0). Firmware sets the stack pointer.
  • Launch args: the launch command's pointer_to_args is delivered in a0 (not ra, despite the SDK docs — verified on device); a0 flows through _start into entry_point's first parameter.
  • No .bss (the linker script asserts it), no heap, no unwinding (panic = "abort").

Publishing

This crate is a separate cargo package from the host crate (and excluded from the host workspace) because it targets RISC-V bare metal. Its library and demo bins use RISC-V inline assembly and a linker script, so they cannot be built for the host target; a crates.io release must therefore verify against the device target:

cargo publish -p et-abi                                        # dependency first
cargo publish --target riscv64gc-unknown-none-elf              # from et-k-rs/

Thanks

Thanks to AiNEKKO https://nekko.ai/ and AI Foundry https://aifoundry.org/ for allowing me time on their community ET-SoC-1 servers to develop this code.

The ET-SoC-1 ET Platform SDK and software emulator can be found on their GitHub: https://github.com/aifoundry-org/et-platform

Licence

Apache-2.0, matching the ET Platform SDK headers this crate binds to.
ET-SoC-1 ET Platform API is under the Apache 2 License.