Sparsr Docs

Host Application Development

Your host application is an ordinary program. It links one shared library, libsparsr_host.so, and uses it to put data on the Sparsr device, run a kernel, and read the answers back. Everything on this page is in include/sparsr.h in the SDK.

The shape of every host program

#include "sparsr.h"

sparsr_kernel_init();                                  /* 1. pick a backend        */
sparsr_write_data_wmem(vector_a, 1);                   /* 2. load the operands     */
sparsr_write_data_wmem(vector_b, 2);
sparsr_load_batch_from_bin("build/my_kernel.spex", 0); /* 3. load the kernel       */
sparsr_execute_batch(0);                               /* 4. start it              */

while (sparsr_read_status() == 0) { }                  /* 5. wait for it to finish */

uint8_t *result = sparsr_read_data_wmem(3);            /* 6. read the answer       */

Six steps, and steps 2 to 6 repeat for as long as you have work. The device is not reset between runs, so anything left in a wide register or a WMEM row is still there.

Building and linking

gcc -I<sdk>/include my_app.c -o my_app \
    -L<sdk>/lib -Wl,-rpath,'$ORIGIN/../lib' \
    -lsparsr_host -lstdc++ -pthread

The rpath is so the binary finds the SDK's libraries without anyone setting LD_LIBRARY_PATH. Point it at wherever lib/ ends up relative to your binary. Both examples in the SDK have a working Makefile, which is the faster thing to copy.

Choosing a backend

SPARSR_BACKEND selects which device runs your kernel. Set it in the environment. Your code never changes.

Value What it is Notes
vm The Sparsr VM Runs RV32I plus the wide instructions. Needs no hardware.
vmproc The same VM, in a process of its own Same kernels as vm, reached over a socket. Slower, but a wedged kernel leaves your program alone.
fpgasim An FPGA simulation For hardware development.
fpgaf2 A real AWS EC2 F2 card Needs the hardware and an FPGA image.
unset, empty, or auto An F2 card if one answers, the VM otherwise See below.

Name the backend, so you always know which device gave you a result. Every example in the SDK does:

SPARSR_BACKEND=vm ./my_app

If you set nothing, the library chooses once, before the first call:

  1. If the F2 backend is not installed, it uses the VM and prints nothing. The SDK does not include the F2 backend, so this is what an SDK install does.
  2. If the F2 backend is installed and a Sparsr card answers in slot 0, it uses the card. Checking for the card only reads its PCI IDs and changes nothing on it.
  3. Otherwise it uses the VM, and prints one line to standard error saying why. You installed the F2 backend, so you expected a card, and the library tells you that you did not get one.

SPARSR_BACKEND=auto does the same thing.

Every software backend runs RV32I. A kernel written in C, or built by spasm from a .S source, runs on vm and vmproc.

If the backend you named cannot answer, there is no device, and no other backend takes its place. This happens when its library is missing from your installation, when it lacks a call, or when SPARSR_BACKEND names a backend that does not exist. Every call then answers "no device":

  • The kernel does not run.
  • A read returns NULL.
  • sparsr_wide_bytes() returns 0.
  • sparsr_read_status() reports the batch as done, with the fault SPARSR_STATUS_FAULT_NO_DEVICE in bits 8-15.

The library prints one line to standard error for each function, the first time it is called, starting libsparsr_host: no device answers sparsr_<name>. It never exits your process, so check for these answers if your program must stop with an error.

Device memory

The device has three memories, and your process's RAM is not one of them. Every transfer is an explicit call.

Memory Reached with Unit What goes in it
IMEM sparsr_load_batch_from_bin, sparsr_load_batch instruction words your kernel
DMEM sparsr_write_data_dmem, sparsr_read_data_dmem 32-bit words scalar arguments, results, your kernel's globals
WMEM sparsr_write_data_wmem, sparsr_read_data_wmem one whole wide vector per row, 4,096 bits through the host library the wide vectors your kernel computes on

Memory Model has the sizes and the layout.

Moving wide vectors (WMEM)

uint8_t  vector[512];                       /* 4,096 bits, one whole wide vector */
sparsr_write_data_wmem(vector, 1);          /* into WMEM row 1 */
uint8_t *out = sparsr_read_data_wmem(3);    /* out of WMEM row 3, 512 bytes */

You always hand over and get back the whole 512 bytes. A WMEM row stores one whole wide vector, at any density, so rows do not overlap and you never have to remember how a row was written.

Compression happens on the wire, not in the row. sparsr_write_data_wmem() compresses the vector before it crosses to the device. A vector with more than 48 non-zero 32-bit lanes is too dense for that, so the call reports an error. The row keeps what it had. For a denser vector, use the raw call, which sends all 512 bytes:

sparsr_write_data_wmem_raw(vector, 0);      /* any density */
uint8_t *out = sparsr_read_data_wmem_raw(0);

Both read calls return the same bytes, so either one reads a row whichever way it was written.

Moving scalars (DMEM)

uint32_t args[2] = { base_row, count };
sparsr_write_data_dmem(args, 0, 2);

This is how you pass arguments to a kernel that was built once and has to work on whatever the host chose. A kernel written in C reads them as ordinary memory at the start of data memory.

Keep transfers few and large

On a card, data crosses the PCIe link between the host and the FPGA, and that link often limits a program more than the kernel does. Move rows in large blocks rather than one at a time, and let the kernel do as much work as it can before you read the results back. The Sparsr VM does not model this cost, so a program that is fast on the VM can still be slow on a card.

Waiting for a kernel

sparsr_read_status() returns 0 while a batch is running and 1 when it has finished. The polling loop in the example above is what the SDK examples do and it is correct for every backend. Bit 0 is the done bit, SPARSR_STATUS_DONE. Bits 8-15 hold a fault code where the backend reports one, and SPARSR_STATUS_FAULT_OF(status) reads it. A batch that faulted still sets the done bit, so the loop always stops.

Other languages

C and C++ are the supported host languages. sparsr.h is guarded for C++, so a C++ program includes it and links the same way.

Any language with a foreign function interface can call libsparsr_host.so, since it is a plain C shared library with no wrapper needed. Sparsr does not currently publish a binding package for any other language.

If you work in Python and your problem is hyperdimensional computing rather than custom kernels, do not write a host program at all. Use torchhd-sparsr, which is a PyTorch device, and skip this whole chapter.

A complete example

examples/01_hello_world_c/ in the SDK is the smallest host program that does something real. Its kernel is two instructions and its host program is 40 lines:

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "sparsr.h"

int main(void)
{
    uint8_t wmem_in[512] = {0};
    uint8_t *wmem_out;
    char kernel_path[30] = "build/hello.spex";

    wmem_in[0] = 42;
    wmem_in[1] = 7;

    sparsr_kernel_init();

    sparsr_write_data_wmem(wmem_in, 1);
    sparsr_load_batch_from_bin(kernel_path, 0);
    sparsr_execute_batch(0);

    while (sparsr_read_status() == 0) {
    }

    wmem_out = sparsr_read_data_wmem(3);

    if (memcmp(wmem_in, wmem_out, 512) != 0) {
        fprintf(stderr, "Output does not match input.\n");
        return 1;
    }

    printf("Hello world check passed.\n");
    return 0;
}

examples/03_kernel_in_c/ is the same idea with a kernel written in C and a real computation in it.

The API Reference describes each of these calls in turn.

Built 2026-10-05 19:25 UTC · c0237a4