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:
- 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.
- 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.
- 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()returns0.sparsr_read_status()reports the batch as done, with the faultSPARSR_STATUS_FAULT_NO_DEVICEin 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.