Blog
[measured on FPGA: all 42 trained matrices, every receipt verified; the activations were test vectors, not a forward pass] An AX7203 computed 28,416 of 28,416 rows of the trained tern_tc model's weight matrices bit-exact, while its harness reported 284,160 authenticated receipts without comparing a single tag. The fixed harness fails on seven kinds of misbehaving cell. On the board it then verified all 403,200 receipts across all 42 ternary matrices, with 33,792 of 33,792 rows bit-exact, plus 51,840 for w_down with int8 activations. Its first long runs stopped when a UART link dropped bytes with 64 jobs in flight. A test registered before it ran put the loss threshold at the USB bridge's 512-byte receive buffer, and at 24 in flight none were lost.
IGLA's board-sized model, tern_tc, has 9.08M parameters. 6,451,200 of them are ternary block weights in 42 matrices across six layers, trained on 2.0B tokens of code to 0.7613 validation bits per byte. The board is an ALINX AX7203 (XC7A200T) running the TRI-NET node cell, built with the open openXC7 flow: a 32-trit ternary dot product with no multiplier and no DSP48. It answers each job with the result, the job's nonce, a node id and a SipHash-2-4 tag over all of them. The host splits every 320-wide row into ten 32-trit jobs and adds up the answers.
| Run (AX7203, 1,144,744 baud) | Jobs | Rows bit-exact | Receipt tags compared? | Time |
|---|---|---|---|---|
| Random 320x320 ternary matvec | 3,200 | 320 / 320 | yes | 0.67 s |
| Layer 0 wq, trained weights | 12,800 | 1,280 / 1,280 | no | 2.7 s |
| wq, wo, gate, up in all six layers | 284,160 | 28,416 / 28,416 | no | 62.4 s |
The activations were ternary vectors, not the model's int8 activations. So this shows the trained weight matrices computed exactly. It is not a forward pass of the model.
The layer harness built each receipt's preimage and then dropped it, and the key it loaded was never used. It counted status byte 0x01 as authentication. It never checked that the nonce came back, and it compared only row sums, so two wrong chunks could cancel. We gave it a software model of the cell that signs every answer with a key the host does not hold. It printed 'receipts authenticated 160/160' and PASS.
What stands: every row value in the table matched the CPU oracle, and the random-matrix run did compare its tags. What is withdrawn: '284,160 receipts authenticated under node0's key', because those receipts were never checked. The fixed harness has since measured all 42 matrices on the board, those 24 included, with every receipt verified (below).
A response now counts only if all five of these hold. Its status is 0x01. Its nonce was issued by this run and answered once. Its node id matches the first answer. Its SipHash tag recomputes under the key. Its y equals that chunk's dot product. A row passes only if every chunk passed and their sum equals the row dot computed directly from the model's int8 weights, not from the packed wire bytes. Each check is shown able to fail:
| What the cell does wrong | Harness verdict |
|---|---|
| signs with another key | rejected: tag |
| returns a wrong answer and signs it validly | rejected: lie |
| flips one tag bit | rejected: tag |
| answers with a nonce the run never issued | rejected: fabricated |
| answers as another node id | rejected: node |
| drops a response | rejected: short read, run stops |
| was never keyed (status 0x04) | no credit, though y is right |
After the board run below, an eighth control was added, for the link rather than the cell. It is a stream that loses 16 bytes in the middle, as the board's link did. The harness credits the answers before the hole, rejects the damaged one and stops.
The cell's own source, trinet_node_core.v and trinet_siphash24.v, unchanged, was simulated in Icarus Verilog at UART bit level. The key was installed over the wire exactly as on the board. The weights are random ternary values in tern_tc's exact shapes, because the trained model file is not in this environment.
| RTL run | Jobs | Rows bit-exact | Receipts verified |
|---|---|---|---|
| Layer 0, all seven matrices, w_down as 27 chunks | 67,200 | 5,632 / 5,632 | 67,200 / 67,200 |
| Layer 5 w_down, int8 activations (6 digit planes) | 51,840 | 320 / 320 | 51,840 / 51,840 |
| Layer 0 wk, node never keyed | 640 | 0 / 64 | 0 / 640 (status 0x04) |
| Layer 0 wk, checked under a different key | 1,280 | 0 / 128 | 0 / 1,280 |
On 2026-09-27 the fixed harness ran on the AX7203 with the trained model file. The host was an M1 Pro, talking to the board's CP2102N UART through a USB hub. The x-vectors are random test vectors from a fixed seed, not the model's real activations.
| Board run (1,144,744 baud) | Jobs in flight | Receipts verified | Rows bit-exact | Result |
|---|---|---|---|---|
| Random 320x320 ternary matvec | 64 | 3,200 / 3,200 | 320 / 320 | PASS, 0.79 s |
| All 42 matrices, ternary x | 64 | 18,984 of 403,200, then the link lost 16 bytes | 1,898 / 33,792 | FAIL, stopped |
| Layer 5 w_down, int8 x | 64 | 9,886 of 51,840, then the link lost 4 bytes | 61 / 320 | FAIL, stopped |
| Layer 5 w_down, int8 x | 8 | 51,840 / 51,840 | 320 / 320 | PASS, 11.9 s |
| All 42 matrices, ternary x | 24 | 403,200 / 403,200 | 33,792 / 33,792 | PASS, 85.5 s |
| All 42 matrices, ternary x (control) | 64 | 6,679 of 403,200, then the link lost 59 bytes | 667 / 33,792 | FAIL, stopped |
| All 42 matrices, ternary x (registered) | 26 | 403,200 / 403,200 | 33,792 / 33,792 | PASS, 85.3 s |
| All 42 matrices, ternary x (registered) | 30 | 149,986 of 403,200, then the link lost 7 bytes | 12,822 / 33,792 | FAIL, stopped |
The window-24 row is the result this post was waiting for. All 42 ternary matrices of the trained model, all 6,451,200 weights, ran on the existing bitstream. The host checked every one of the 403,200 receipts: status, nonce, node id, the SipHash tag recomputed under the key, and y. All 33,792 rows equal the int8-weight oracle. The run took 85.5 s, 4,716 answers per second.
The w_down run at 8 in flight is the first board run of trained weights with int8 activations and every receipt checked: 51,840 tags and 320 rows. It covers one matrix of 42, with one random activation vector.
The four failures are not wrong answers. All 185,535 answers that arrived whole before a hole had the right y and a verifying tag. The one answer each hole tore through failed a check and was refused. The link is what failed. The answer stream lost 16, 4, 59 and 7 bytes, with intact bytes on both sides. The cell cannot produce that pattern, because it sends every answer whole from one buffer. The harness did its job: it stopped at the first unframed read and credited nothing after it.
The window matters; the control shows it. The same full run, on the same harness, setup and session, went clean at 24 jobs in flight and lost bytes after 6,744 jobs at 64. So the pass comes from the window, not from the new harness's timing and hex output. At 64, three long runs slipped three times.
The first explanation does not survive the control. The idea was that answers pile up while the harness process looks away. The fixed harness measures those pauses. In the failing control at 64 the longest was 4.2 ms, about 1,700 answers before the hole. Passing runs survived pauses of 22.4 ms at 24 in flight and 25.8 ms at 26.
What does fit is the USB bridge. The board's UART goes to the host through a CP2102N, whose datasheet gives a 512-byte receive buffer and asks for handshaking above 1 Mbaud to avoid receiver overrun. The node runs at 1,144,744 baud and has no handshake lines, so nothing can hold it off. With W jobs in flight, at most 19 x W answer bytes are on their way. The next test was registered before it ran: a pass at 26 in flight (494 bytes) and a slip at 30 (570 bytes). Both happened. Window 26 carried all 403,200 jobs, and window 30 lost 7 bytes after 150,017 jobs. That puts the loss threshold between 494 and 570 bytes, around the 512-byte buffer.
One side prediction missed. The slip at 30 came after 150,017 jobs, not within the first 20,000 as at 64, so the stalls that fill the buffer are rarer than a single fixed gap would explain. What stalls the bridge's USB transfers is not measured, and the hub is not excluded. The rule the harness follows now is to keep 19 x W under 512 bytes, which is why the default is 24 (456 bytes). It costs no measurable throughput: 4,716 answers/s, against about 4,660 to 4,750 at 64 before the slips.
w_down was left out because its input is 864 wide and the plan called for a wider cell. It does not need one: 864 = 27 x 32, so it is 27 jobs per row on the same cell. wk and wv have 320-wide inputs and were simply skipped. Together that is all 42 ternary matrices and all 6,451,200 weights on the existing bitstream.
int8 activations were Stage B.3, which planned new RTL. Every integer from -364 to 364 is a sum of six balanced-ternary digits: q = sum of 3^k d_k with each d_k in {-1, 0, +1}. So w.q = sum of 3^k (w.d_k), and each w.d_k is an ordinary ternary job. Six jobs per chunk replace a new datapath. The cell stays ternary, and the host applies the powers of three in the open.
Work with me
I audit RTL and build independent, bit-exact models, then take the result through synthesis and, when useful, onto an Artix-7 board. The first conformance module is free.