Skip to content

fpga(ax7203): ternary mat-vec over real tern-tc-9m weights in DDR3, UART receipt (Closes #10461) - #10475

Open
gHashTag wants to merge 9 commits into
masterfrom
fpga/cell26-ternary-matvec
Open

gHashTag wants to merge 9 commits into
masterfrom
fpga/cell26-ternary-matvec

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Closes #10461

Cell 26 of specs/igla/own_model.t27 ("Full model on FPGA, DDR3"), epic #10024. #10248 measured the DDR3 read path on the AX7203. This PR adds the compute step on top of it: real ternary weights in DDR3, a mat-vec computed from them on the board, and a UART receipt that is checked against the spec's own test.

Board receipts (AX7203, xc7a200tfbg484-2, 2026-10-18)

run bitstream bytes uploaded receipt
1 design, seed 8 real weights T27 TERN N=0000000000000280 S=4B58160720ABB863 C=4B58160720ABB863 Y=9A50D884522C233D E=0000000000000000 F=0000000000000000 L=0000000000000001 PASS
2 design, seed 8 byte 0 changed FF -> 01 T27 TERN N=0000000000000280 S=4B58160720ABB81C C=4B58160720ABB81C Y=9A50D884522C2320 E=0000000000000000 F=0000000000000000 L=0000000000000001 FAIL
3 mutant (FF read as +1), seed 1 real weights T27 TERN N=0000000000000280 S=4B58160720ABB863 C=4B58160720ABB863 Y=3E5E6C8ED51DF5C8 E=0000000000000000 F=0000000000000000 L=0000000000000001 FAIL
  • Run 1 prints exactly the S and Y the data spec computes. The spec values were read off its own failing assert_eq with 0 in their place, not typed in from the board (FR-002).
  • Each negative control fails for the predicted reason, and on exactly the predicted fields:
    • Run 3 differs only in Y. The mutant decodes the trits wrongly but stores the same bytes.
    • Run 2 differs in S, C and Y. Its S and Y are S_FLIP0/Y_FLIP0, which the spec computed before the board run.
    • The mutant's Y equals Y_MUT_FF. That value was read off the spec after the board printed it, and the spec comment says so.
  • Neither control has a bad trit or a bad frame (E=F=0). A FAIL there is the verdict on the value, not on a lost byte.

How each run was done:

  • Flash with openFPGALoader -c digilent_hs2 --ftdi-serial 210512180081 <bit>.
  • Once the BIOS reaches the litex> prompt, upload with dd ... bs=512 to /dev/cu.usbserial-130 at 115200 8N1.
  • Read the receipt with cat.

Bitstreams flashed:

  • design seed 8: e1afe42f028ca7bd29ae6a5eb17fd464b5436bded637e065269259ec3ef4f583
  • mutant seed 1: 47c0f05d147182ddffd64226f625f9f929d27d80c47e690203b673ae23d018b7

Bytes uploaded:

  • real: 970a7f55e7da107a657d6c6b61e46167d919e070fd906a1265294e688c216fd0
  • byte 0 flipped: 3535984d233655a38e0c963af0dc2520c09385c56e936a12fea5bd1864a19dc8

The first board run found a spec bug (fixed in b0d3fc8)

The first route that calibrated printed the right N, S, C and Y with E=F=0, and still said FAIL. s_want and y_want reset to 0. The tests assigned them before each run, so the spec passed while the hardware compared a correct receipt against 0.

Both registers now reset to the pinned constants. A new first test asserts the values out of reset, before any test assigns them, and that test fails with the old = 0 initializers. The fix is in the spec, not in the hardware: the board found the gap, and the spec now has the test that catches it.

DDR3 calibration is route luck

An all-zero FAIL receipt is not a data verdict. In those runs core_init_error is set: the LiteDRAM BIOS read leveling finished but the memtest failed, and the state machine reports V_FAIL with nothing computed.

Whether a route passes the memtest depends on the route:

  • Design, this head's spec: seeds default, 1, 2, 3, 4, 5, 6 and 9 failed the memtest. Seed 8 passed.
  • Mutant: seeds 2 and 3 failed the memtest. Seed 1 passed.
  • Design seeds 12 and 13 and mutant seeds default and 7 failed in nextpnr itself.

The clk Fmax per seed, from nextpnr-xilinx on the Railway lab:

seed design mutant
default 84.85 MHz PnR fail
1 84.35 87.36 (calibrates)
2 78.27 88.75
3 89.69 85.36
4 81.02 78.43
5 90.37 82.93
6 86.44 87.86
7 86.14 PnR fail
8 87.08 (calibrates) 88.21
9 88.37
10 81.16
11 84.53
14 81.23

No route meets 100 MHz. The critical path is in the LiteDRAM core's VexRiscv BIOS CPU (core.VexRiscv.lastStageIsValid), not in the mat-vec. Fmax does not predict calibration: seed 5 is the fastest design route and it fails the memtest. Design seeds 10, 11 and 14 and mutant seeds 4, 6 and 8 were built but not flashed. Two working routes were enough.

What is in it

  • specs/fpga/ddr3_ternary_matvec.t27 is the board spec. It uses the same LiteDRAM core, clock and pins as ddr3_head_bandwidth.t27.
    • It receives 20,480 weight bytes on UART RX (P20, 115200 8N1) and packs them into 640 beats of 256 bits.
    • It writes them to DDR3, reads them back, and computes y = W x: 64 rows x 320 trits, wrapping 32-bit sums, x_c = (37c + 11) mod 256 as an int8.
    • The receipt goes out on N15: T27 TERN N=<beats> S=<write sum> C=<read sum> Y=<output sum> E=<bad trits> F=<bad frames> L=<line> PASS|FAIL
  • specs/fpga/tern9m_l0_k_weights.t27 is the data spec.
    • The tensor is blocks.0.attn.k.weight of playra/tern-tc-9m, from c_infer/model.bin (16,953,800 bytes, sha256 3102abdf35057924e077a86db4fdac726e4fe1f6574df47f39a638ba99a3ce9c): offset 10,589,476, 20,480 bytes, held as 2,560 u64 words.
    • Its tests compute S and Y from those words with the board spec's own trit_term and the shared fold, and assert the board spec's constants. They also pin the two FAIL receipts the negative controls print.
    • The head (wte) is tied and f32, so the PR uses the first ternary projection of the same checkpoint.
  • specs/fpga/board_receipt.t27 is a pure module (no pins, no state) holding fold, hex_digit and pass_fail_char.
    • rc_filter_ax7203, ddr3_head_bandwidth and the new board spec each had their own copy of these, and dupe-bodies flagged them. Now each one uses this module.
    • ddr3_head_bandwidth keeps its own four-verdict verdict_char for the SLOW and KILL lines.
    • The ddr3_head_bandwidth and rc_filter_ax7203 netlists emitted after the move equal the ones from before it, up to line order, comments and the moved function's name.
  • The seals of all touched specs and the t27b ledger rows, written on the Railway lab.

Spec results (Railway lab)

  • ddr3_ternary_matvec.t27: 10/10 pass, 0 vacuous. Asserts per test: 5, 3, 14, 321, 5, 50, 12, 2, 640, 8.
  • tern9m_l0_k_weights.t27: 5/5 pass, 0 vacuous. Asserts per test: 4, 3, 1, 2, 6. The tests check:
    • the byte census: 8461 zeros, 5983 +1, 6036 -1, 0 other;
    • S and Y;
    • that the FF-as-+1 mutant misses Y and lands on Y_MUT_FF;
    • that byte 0 changed to 01 gives S_FLIP0/Y_FLIP0, and restoring FF gives the expected values back.
  • board_receipt.t27: 4/4 pass, 0 vacuous.
  • The mutant spec (FF read as +1) fails the board spec's own trit test.
  • Bitstreams were built from these specs on the Railway lab with t27b verilog, yosys, nextpnr-xilinx (xc7a200tfbg484-2), fasm2frames and xc7frames2bit. Every stage returned rc 0 except the PnR failures listed above.

Review

Cell 26 is named in own_model.t27 only after the reviewer bee approves this head (FR-006). That is why this PR carries no-auto-merge.

CI notes

This PR adds no code in any language other than t27.

🤖 Generated with Claude Code

…ART receipt (Closes #10461)

Cell 26 of specs/igla/own_model.t27 (epic #10024), the compute step on top of
the DDR3 read path of #10248.

- specs/fpga/ddr3_ternary_matvec.t27: the board spec. It receives the 20,480
  bytes of blocks.0.attn.k.weight (tern-tc-9m, sha256 3102abdf...ce9c, offset
  10,589,476) on UART RX P20, packs them into 640 beats of 256 bits, writes them
  to DDR3, reads them back one beat at a time and steps through their trits one
  per clock: y_r = sum_c w_rc * x_c, x_c = int8(37c + 11), wrapping 32 bits.
  Receipt on N15: T27 TERN N= S= C= Y= E= F= L= PASS|FAIL.
- specs/fpga/tern9m_l0_k_weights.t27: the same bytes as 2,560 u64 words made
  with od, and the tests that compute S_EXPECT and Y_EXPECT from them with the
  board spec's own fold and trit_term; the FF-as-+1 mutant and a one-byte
  change both miss.
- Tests on the Railway lab: 9/9 and 5/5, 0 vacuous. A copy of the board spec
  with the FF trit read as +1 fails its own test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 13:51:30 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 50
PRs with All Checks Green 0
READY 0
FAILING 50
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

…cs shared (Refs #10461)

fold, hex_digit and the PASS/FAIL verdict characters were written three times,
in rc_filter_ax7203, ddr3_head_bandwidth and ddr3_ternary_matvec; dupe-bodies
named them. They now live in one pure module, specs/fpga/board_receipt.t27
(4 tests, 0 vacuous), and each board spec uses it. ddr3_head_bandwidth keeps
its own four-verdict verdict_char for the SLOW and KILL lines.

The head_bandwidth and rc_filter netlists emitted after the move equal the
ones from before it, up to line order, comments and the function name.

Seals written on the Railway lab with zig on PATH; three t27b ledger rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 14:06:04 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 48
PRs with All Checks Green 2
READY 0
FAILING 48
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 48 + 0 + 0 = 48, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 14:20:43 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 50
PRs with All Checks Green 0
READY 0
FAILING 50
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

board_receipt.t27 now holds the one hex_digit the three AX7203 board specs
had each written, so the group is gone and the gate asked for the ceiling to
come down with it: 1 line removed by tools/dupe_scan.py --bless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 14:36:58 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 48
PRs with All Checks Green 2
READY 0
FAILING 48
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 48 + 0 + 0 = 48, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

…#10461)

Both specs arrive with cell 26 and gen-verilog emits a module for each, so the
ratchet read them as NEW. board_receipt elaborates clean. The 8 in the weights
spec are all names it takes by `use` from board_receipt and from
ddr3_ternary_matvec (a t27b spec gen-verilog skips); the weights are test data
the board receives over the UART, never synthesized. Measured on CI (iverilog
12.0) and on the Railway lab (11.0): 214 errors, baseline now 214.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 15:04:58 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 50
PRs with All Checks Green 0
READY 0
FAILING 50
PENDING 0
NO CHECKS YET 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

…print (Refs #10461)

The mutant test only said Y != Y_EXPECT and the byte-0 test only said
S and Y differ. Both now also assert the exact values, read off the
spec's own failing assert_eq with 0 in their place, not off the board:

- Y_MUT_FF = 0x3E5E6C8ED51DF5C8, Y of the FF-as-+1 mutant on the real bytes
- S_FLIP0  = 0x4B58160720ABB81C, Y_FLIP0 = 0x9A50D884522C2320, the real
  design on the bytes with byte 0 changed from FF to 01

On the AX7203 the mutant bitstream printed Y=3E5E6C8ED51DF5C8 FAIL and
the design printed S=C=4B58160720ABB81C Y=9A50D884522C2320 FAIL, so
each negative control fails for the predicted reason.

5/5 pass, 0 vacuous (asserts 4, 3, 1, 2, 6); resealed on the Railway lab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 16:43:06 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 48
PRs with All Checks Green 2
READY 0
FAILING 48
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 48 + 0 + 0 = 48, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 17:01:15 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 46
PRs with All Checks Green 4
READY 2
FAILING 46
PENDING 0
NO CHECKS YET 0

These columns do not partition: 2 + 46 + 0 + 0 = 48, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

This was referenced Oct 18, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-auto-merge Opt out of the issue gate's default squash auto-merge (issue-gate.yml)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fpga(ax7203): own-model cell 26 -- ternary mat-vec over real tern-tc-9m weights in DDR3, UART receipt

1 participant