Sparsr Docs

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
Built 2026-10-05 19:25 UTC · c0237a4