
TOPHAT is a hardware decision-tree inference engine for a fixed depth-3 tree (7 internal nodes, 8 leaves) with 8 input features.
A decision tree is a supervised learning model that predicts an output by applying a sequence of simple feature-based tests (for example, feature_i < threshold). Training determines which tests to use and where to place them so the model separates examples effectively.
At inference time, prediction traverses from the root to a leaf based on comparison results. In hardware, the model is represented as fixed node parameters plus deterministic control flow.
For a well-known example of the kind of problem to which a decision tree can be applied, see Kaggle's Titanic - Machine Learning from Disaster.
TOPHAT consumes a fixed 22-byte model image:
| Field | Bits | Note |
|---|---|---|
| feature_id | 3 | Supports 8 features |
| threshold | 8 | Unsigned |
Child selection is implicit from node index i (dense full binary tree):
2*i + 12*i + 2Any model can be used as long as it is serialized into the format above. The example below was generated with scikit-learn. See test/generate_golden_tree.py for details. For more background on scikit-learn trees, see the scikit-learn Decision Trees documentation.
| Byte Range | Record | Decoded | Hex Bytes |
|---|---|---|---|
| 0..1 | N0 | [feature=2, threshold=24] | 02 18 |
| 2..3 | N1 | [feature=0, threshold=8] | 00 08 |
| 4..5 | N2 | [feature=7, threshold=35] | 07 23 |
| 6..7 | N3 | [feature=4, threshold=20] | 04 14 |
| 8..9 | N4 | [feature=1, threshold=20] | 01 14 |
| 10..11 | N5 | [feature=6, threshold=40] | 06 28 |
| 12..13 | N6 | [feature=3, threshold=28] | 03 1C |
| 14..21 | Leaves | [L0..L7] output values | 08 12 19 2D 37 55 5F 8C |
TOPHAT feature input is a fixed 8-byte vector with each feature encoded as an unsigned 8-bit value. In the Titanic demo, the bytes encode passenger class, sex, age, siblings/spouses aboard, parents/children aboard, fare, port of embarkation, and whether the passenger is traveling alone, in that order.
Example Feature Serialization
| Feature ID | Passenger Attribute (example value) | Hex Byte |
|---|---|---|
| 0 | Passenger class (3rd class) |
FF |
| 1 | Sex (male) |
FF |
| 2 | Age (22 years) |
45 |
| 3 | Siblings/spouses aboard (1) |
20 |
| 4 | Parents/children aboard (0) |
00 |
| 5 | Ticket fare (7.25) |
04 |
| 6 | Port of embarkation (Southampton) |
00 |
| 7 | Traveling alone (no) |
00 |
TOPHAT uses a byte-wide command and payload interface:
| Signal | Direction | Description |
|---|---|---|
ui_in[7:0] |
input | Payload byte |
uio_in[0] |
input | valid strobe |
uio_in[2:1] |
input | Command select |
uio_out[3] |
output | ready (~busy) |
uio_out[4] |
output | busy |
uio_out[5] |
output | pred_valid pulse |
uio_out[6] |
output | model_loaded |
uio_out[7] |
output | error_or_missing_features (`error |
A transfer is accepted on a rising clock edge only when valid=1 and ready=1.
uio_in[2:1] |
Name | ui_in[7:0] payload |
|---|---|---|
2'b00 |
CMD_MODEL |
Model byte stream |
2'b01 |
CMD_FEATURE |
Feature byte stream |
2'b10 |
CMD_CTRL |
Bit0=run, bit1=clear |
2'b11 |
Reserved | Ignored |
ready=1.CMD_MODEL (2'b00), one accepted transfer per byte.0..13 are node records, bytes 14..21 are leaf values. See "Model Representation" above.model_loaded (uio_out[6]) asserts after the 22nd byte is accepted.Notes:
CMD_CTRL with clear=1 (ui_in[1]=1) resets model, features, status, and core state.model_loaded until all 22 bytes are reloaded.CMD_FEATURE (2'b01), byte 0 through byte 7. See "Feature Representation" above.ready=1, then send CMD_CTRL with run=1 (ui_in[0]=1).busy=1 traversal and returns with a one-cycle pred_valid pulse.uo_out[7:0] when pred_valid=1.features_loaded, so features must be reloaded before the next prediction.| Status bit | Meaning |
|---|---|
uio_out[3] |
ready = ~busy |
uio_out[5] |
One-cycle pred_valid when a result is produced |
uio_out[7] |
error OR ~features_loaded |
| Run condition | Behavior |
|---|---|
model_loaded=1 and features_loaded=1 |
Normal traversal, valid prediction on uo_out |
| Missing model or features | Run is rejected, error is set |
The test suite lives in test/ and requires cocotb, Icarus Verilog, and the Python packages listed in test/requirements.txt. From a virtualenv:
cd test
pip install -r requirements.txt
RTL simulation (cocotb + Icarus Verilog):
make test-cocotb # builds the sim and runs test/test.py under cocotb
The cocotb test (test/test.py) performs a full end-to-end check:
CMD_CTRL clear.golden_model.bin) byte-by-byte with CMD_MODEL and waits for model_loaded.CMD_FEATURE, sends CMD_CTRL with run=1, and reads the prediction from uo_out on the pred_valid pulse.Python-only tests (no simulator needed):
make test-python # runs pytest on test_golden_model.py, test_host_client.py, etc.
These cover:
test_golden_model.py): verifies the scikit-learn decision tree reproduces every fixture case.test_host_client.py): validates model/feature payload formatting, length checks, and CLI argument parsing against a fake transport.test_host_transport.py): tests JSON-line serial framing, echo filtering, and timeout behavior with a fake serial port.test_example_assets.py): ensures the published example fixtures and model image match the test golden references.Run everything (simulation + Python tests):
make test-all # clean build, then runs cocotb and pytest
With the design active on a Tiny Tapeout board and the RP2040 bridge firmware running:
/dev/ttyACM0).tools/host/tophat_host.py) to interact:# Check the bridge is alive
python tools/host/tophat_host.py ping --port /dev/ttyACM0
# Clear any previous state
python tools/host/tophat_host.py clear --port /dev/ttyACM0
# Load the 22-byte model image
python tools/host/tophat_host.py load-model --port /dev/ttyACM0 --model test/golden_model.bin
# Run a prediction with an inline feature vector
python tools/host/tophat_host.py predict --port /dev/ttyACM0 --features 4,6,10,12,15,20,10,18
The predict command loads the 8 feature bytes, triggers inference, and prints the prediction byte returned by the hardware.
You can also supply features from a JSON file:
python tools/host/tophat_host.py predict --port /dev/ttyACM0 --features-file features.json
Where features.json is either a flat list ([4,6,10,12,15,20,10,18]) or an object with keys feature_00 through feature_07.
Thanks to BLAKE2s Hashing Accelerator: A Solo Tapeout Journey for the inspiration.
| # | Input | Output | Bidirectional |
|---|---|---|---|
| 0 | data_i[0] | prediction[0] | valid_i |
| 1 | data_i[1] | prediction[1] | cmd_i[0] |
| 2 | data_i[2] | prediction[2] | cmd_i[1] |
| 3 | data_i[3] | prediction[3] | ready_o |
| 4 | data_i[4] | prediction[4] | busy_o |
| 5 | data_i[5] | prediction[5] | pred_valid_o |
| 6 | data_i[6] | prediction[6] | model_loaded_o |
| 7 | data_i[7] | prediction[7] | error_or_missing_features_o |