Sparsr Instruction Set Architecture (ISA)
Sparsr is a 32-bit RISC-V core with a second register file of 32 registers that are each 8,192 bits wide, and one custom opcode that operates on them. A kernel is ordinary RV32I code around a few wide instructions, each of which does one bitwise operation across a whole register at once.
This page is the reference for both halves: the scalar instruction set, which is the standard one, and the wide instruction set, which is Sparsr's own. For every instruction it says whether the Sparsr VM runs it and whether today's FPGA hardware does, because those are different answers and the difference matters when you plan an evaluation.
Two implementations, one page
| Column | What it is |
|---|---|
| VM | The Sparsr VM, the software model of the processor that is in the SDK. It runs on your machine. It is the reference: what the VM does is what the ISA means. |
| Hardware | The FPGA image. Its scalar side decodes a subset of RV32I and its wide side decodes custom-0, the encoding on this page. Not every operation is built yet, and the tables say which. A kernel that stays inside the built subset runs on the VM and on a card unchanged. |
If you are writing a kernel today, write it against the VM, in C through
sparsr_intrinsics.h or in RISC-V assembly through sparsr.S. That is the
encoding the hardware decodes, and it is what every example in this
documentation uses. The older MIPS-format assembly was removed, because no
Sparsr device decodes it.
Programmer's model
| Item | Value |
|---|---|
| Scalar registers | x0 to x31, 32 bits each. x0 reads as zero. |
| Wide registers | w0 to w31, 8,192 bits each. Today's FPGA hardware still has narrower ones. SPARSR_WIDE_BITS in the SDK headers says which width a kernel is built for. |
pc |
32 bits, byte-addressed. Instructions are 4 bytes and 4-byte aligned. |
| Instruction memory | 64 KiB, starting at address 0. Today's FPGA hardware has less. A batch is loaded here and starts at its first word. |
| Data memory | 64 KiB, starting at address 0x80000000. Today's FPGA hardware has less. Reached with the RV32I loads and stores. A load may not read the instruction range. |
| Wide memory (WMEM) | Row-addressed storage for wide vectors, used by the wide load and store. A row stores one whole wide register, raw. See WMEM, XMEM and the DMA. |
| WMEM byte window | The WMEM rows, starting at address 0x90000000, read and written with the RV32I loads and stores. Row n starts at 0x90000000 + n * SPARSR_WIDE_BYTES. VM only today. |
| Endianness | Little-endian, in memory and in the image. |
| Arithmetic exceptions | None. Overflow is ignored. |
| Delay slots | None. |
The scalar side targets the ratified RVI20U32 profile: RV32I with ecall,
fence and fence.tso, and no extension. There is no M, no C, no F, and
no CSR of any kind.
Scalar instructions: RV32I
The formats are the six standard RISC-V ones (R, I, S, B, U, J) and are not repeated here. Any RV32I reference has them. The VM runs all 42 RVI20U32 encodings, and passes the RISC-V Architectural Compatibility Tests for RV32I with every result byte-identical to the Sail reference model.
Where RV32I leaves a behaviour open, Sparsr fixes it as follows.
| Behaviour | Sparsr's answer |
|---|---|
ecall |
Ends the batch and returns to the host, which can read pc and x10 to x17. |
ebreak |
Ends the batch and returns to the host, reported differently from ecall. GCC emits it for __builtin_trap(), so it reads as a failure. |
fence, fence.tso, pause |
Execute as a no-op. One core, no cache, and the host does not touch memory during a batch, so there is nothing to order. |
| Misaligned load or store | Fatal trap. |
| Reserved or illegal instruction | Fatal trap. Never a silent no-op. |
| Access outside the implemented memory | Fatal trap. So is a load from the instruction range. |
| Branch or jump to a misaligned address | Fatal trap, reported on the branch. |
A fatal trap ends the batch and tells the host why. The core does not run past it.
On hardware, ecall, ebreak and every other SYSTEM encoding (ecall,
ebreak, or anything privileged-mode or Zicsr needs, which this core has
none of) all stop the batch the same one way today - there is no CSR or
mcause machinery yet to report which one happened, so nothing in the
status the host reads back distinguishes ecall from ebreak from an
illegal SYSTEM word, unlike the VM. fence and fence.tso do execute as
a plain no-op on hardware, matching the VM; fence.i (Zifencei) is out of
scope and stops the batch like any other unimplemented instruction.
The hardware's scalar subset today
The FPGA's scalar decoder covers a subset of RV32I. The current image does not decode anything outside this table. The VM runs all of it.
| Group | Instructions | Hardware |
|---|---|---|
| Register-register | add, sub, and, or, xor |
✅ |
| Register-immediate | addi, andi, ori, xori |
✅ |
| Load and store | lw, sw |
✅ |
| Shifts | sll, srl, sra and the immediate forms |
❌ |
| Comparisons | slt, sltu, slti, sltiu |
❌ |
| Upper immediates | lui, auipc |
❌ |
| Branches and jumps | beq, bne, blt, bge, bltu, bgeu, jal, jalr |
❌ |
| Sub-word memory | lb, lh, lbu, lhu, sb, sh |
❌ |
| System | ecall, ebreak, fence, fence.tso |
✅ |
Wide instructions: custom-0
The wide instructions share one opcode, RISC-V custom-0 (0001011), and use
the standard RISC-V formats. funct3 selects the class, and inside a class the
funct7 field selects the operation. A register field indexes the wide file or
the scalar file depending on the class and, for one field, on the reduce mode.
funct3 |
Class | Format | What it is |
|---|---|---|---|
000 |
WOP | R | Wide ALU operation, with an optional reduction of the result. |
001 |
WMULTI | R4 | Two-destination operations. One is defined, wcsa. |
010 |
WSHIFT | R | Per-block shift and rotate. The encoding is fixed. The operation is not built yet. |
011 |
reserved | - | Fatal trap. |
100 |
WL | I | Wide load: one WMEM row into a wide register. |
101 |
reserved | I | See the note under WL and WS. |
110 |
WS | S | Wide store: one wide register into a WMEM row. |
111 |
reserved | S | See the note under WL and WS. |
WOP: the wide ALU
31 25 24 20 19 15 14 12 11 7 6 0
+------------+--------+--------+------+--------+--------------+
| funct7 | rs2 | rs1 | 000 | rd | 0001011 |
| mode | f | ws2 | ws1 | | wd/xd | custom-0 |
+------------+--------+--------+------+--------+--------------+
The core computes f(ws1, ws2) across the whole register, then applies mode to
the result, then writes it. rs1 and rs2 always name wide registers. rd
names a wide register when the mode writes a vector, and a scalar register when
the mode reduces to a number.
The function, funct7[3:0]. On hardware, an operation that is allocated
but not built stops the batch when the decoder meets it, as a reserved one
does.
f |
Mnemonic | Operation | VM | Hardware |
|---|---|---|---|---|
0000 |
wand |
ws1 & ws2 |
✅ | ✅ |
0001 |
wor |
ws1 | ws2 |
✅ | ✅ |
0010 |
wxor |
ws1 ^ ws2 |
✅ | ✅ |
0011 |
wnor |
~(ws1 | ws2) |
✅ | ✅ |
0100 |
wadd |
Lane-wise add | ❌ Trap: not implemented. | ❌ Stops the batch: not built. |
0101 |
wsub |
Lane-wise subtract | ❌ Trap: not implemented. | ❌ Stops the batch: not built. |
0110 |
wxnor |
~(ws1 ^ ws2) |
✅ | ❌ Stops the batch: not built. |
0111 |
wnot |
~ws1. rs2 must be 00000. |
✅ | ❌ Stops the batch: not built. |
1000 |
wandn |
ws1 & ~ws2 |
✅ | ❌ Stops the batch: not built. |
1001 |
wrnd |
A pseudo-random wide word. rs1 and rs2 must be 00000. Each design chooses its own generator. |
✅ | ❌ Stops the batch: not built. |
1010 to 1111 |
- | Reserved. Fatal trap. |
The reduce mode, funct7[6:4]. Modes 100 to 111 write a number into a
scalar register. The others write a vector into a wide register. On hardware
only mode 000 is built. Any other mode stops the batch.
mode |
Meaning | Writes | VM | Hardware |
|---|---|---|---|---|
000 |
No reduction: write f(ws1, ws2). |
wide | ✅ | ✅ |
001 |
Segmented per-lane counts | wide | ✅ | ❌ |
010 |
Prefix rank | wide | ✅ | ❌ |
011 |
Threshold-accumulate over N vectors | wide | ✅ | ❌ |
100 |
Population count of the whole result | scalar | ✅ | ❌ |
101 |
Parity of the whole result | scalar | ✅ | ❌ |
110 |
OR-reduce: is any bit set (wany) |
scalar | ✅ | ❌ |
111 |
AND-reduce: are all bits set (wall) |
scalar | ✅ | ❌ |
So a Hamming distance is one instruction, wxor with mode 100, and a GF(2)
syndrome bit is wand with mode 101.
WMULTI: wcsa
funct3 = 001, R4 format. The one operation defined, funct2 = 00, is the
fused carry-save accumulate: it writes ws1 ^ ws2 to rd and ws1 & ws2 to
the register named by rs3, in one instruction. It is what makes a bit-plane
counter cheap. Every other funct2 value is reserved and traps.
| VM | Hardware | |
|---|---|---|
wcsa |
✅ | ❌ Stops the batch: the class is not built. |
WSHIFT: per-block shift and rotate
funct3 = 010, R format. funct7[3:0] selects rotate left, rotate right,
shift left or shift right. inst[31] says whether the amount comes from a wide
register (one amount per block) or a scalar register (one amount for every
block). The block size is fixed by the design, not by the instruction. The encoding is frozen
and nothing executes it yet: the VM decodes it and reports an unimplemented
instruction, and the hardware decodes it and stops the batch. Several fields want it,
because binding in hyperdimensional computing and quasi-cyclic codes both need
a rotate.
WL and WS: wide load and wide store
31 20 19 15 14 12 11 7 6 0
+--------------+--------+------+--------+--------------+
| imm[11:0] | rs1 | 100 | rd | 0001011 | wl wd, disp(rs1)
| row displace.| GPR | | wide | custom-0 |
+--------------+--------+------+--------+--------------+
31 25 24 20 19 15 14 12 11 7 6 0
+------------+--------+--------+------+--------+--------------+
| imm[11:5] | rs2 | rs1 | 110 |imm[4:0]| 0001011 | ws ws2, disp(rs1)
| | wide | GPR | | | custom-0 |
+------------+--------+--------+------+--------+--------------+
The WMEM row is rs1 + displacement, a scalar base register plus a signed
12-bit row number, exactly as lw and sw address bytes. With rs1 = x0 the
displacement is an absolute row, which is the form the earlier WL/WS had;
the register-indirect form, the earlier WLR/WSR, is the same instruction
with a base register. They are no longer separate encodings.
| VM | Hardware | |
|---|---|---|
wl, ws with rs1 = x0 |
✅ | ✅ |
wl, ws with a base register |
✅ | ✅ The base is read from the register file with no forwarding, so a base computed by the instruction just before needs an instruction between them. |
A wide load and store move raw rows. A row holds a whole register, so any
vector can be stored, however dense. On the VM, funct3 classes 101 and 111
are illegal instructions, as the encoding freeze leaves them. Today's FPGA
hardware still stores a row compressed, which limits how dense a stored vector
can be.
Reserved encodings
Every custom-0 encoding not listed above is an illegal instruction and takes
a fatal trap: reserved funct3 classes, reserved function values, reserved
reduce or shift fields. The batch stops and the host is told. It is never a
silent no-op, for the same reason RV32I's reserved encodings are not.
Batch start and batch end
The host loads a kernel image into instruction memory and starts it at word 0.
The core sets sp and ra before it runs, so a C kernel's kernel_main is an
ordinary function: returning from it ends the batch with status 0, and
_sparsr_exit(n) ends it early with status n. ecall and ebreak end it
too, as above, and so does any fatal trap.
The all-ones word 0xFFFFFFFF was the batch terminator of the MIPS-era
design. On the VM it is an illegal instruction and traps. Hardware now sets
sp and ra the same way the VM does, so a kernel meant for a card can end
the same way: by returning from kernel_main. Hardware still accepts the old
terminator too, so an existing image built around .word 0xFFFFFFFF keeps
working unchanged.
Writing wide instructions
In C, include sparsr_intrinsics.h and use the intrinsics; the header
emits the encodings above through .insn, so a stock RISC-V GCC builds a
kernel with no assembler support at all. _sparsr_wand, _sparsr_wor,
_sparsr_wxor, _sparsr_wnor, _sparsr_wxnor, _sparsr_wnot,
_sparsr_wandn and _sparsr_wrnd are the functions; _sparsr_wpopcount,
_sparsr_whamming, _sparsr_woverlap, _sparsr_wany, _sparsr_wall and
_sparsr_wreduce are the reductions; _sparsr_wl, _sparsr_ws,
_sparsr_wlr, _sparsr_wsr are the loads and stores; _sparsr_wcsa is the
carry-save accumulate.
In assembly, .include "sparsr.S" and write the mnemonics in the tables:
wand, wor, wxor, wnor, wxnor, wnot, wandn, wrnd, wl, ws,
wlr, wsr and wcsa, each a macro that expands to one .insn. spasm
assembles a .S file with the stock RISC-V assembler. See
Writing a Kernel in RISC-V Assembly
for a walk-through.
Example
An RV32I kernel. It loads two vectors and stores their AND, OR and XOR, then
hands the host a Hamming distance in a0. The macros take register numbers,
so w3 is written 3 and a0 is written 10:
.include "sparsr.S"
.text
.globl kernel_main
kernel_main:
wl 1, 1 # WMEM row 1 into w1
wl 2, 2 # WMEM row 2 into w2
wand 3, 1, 2 # w3 = w1 & w2
wor 4, 1, 2 # w4 = w1 | w2
wxor 5, 1, 2 # w5 = w1 ^ w2
ws 3, 3 # w3 into WMEM row 3
ws 4, 4
ws 5, 5
wop 2, 4, 10, 1, 2 # wxor with the popcount mode: distance into a0
ret # end of batch, status 0