Skip to content

Commit d921d9e

Browse files
author
gHashTag
committed
feat(horizon-B): gf24/gf32 pipelined decode (iverilog 30k/30k) + takum reassessment
ТРЕК 2 (HW-prep горизонт B): - fpga/openxc7-synth/gf_decode_param_pipe.v: 2-стадийный конвейер gf{N} decode (латентность 2, арифметика бит-в-бит = gf_decode_param.v). gf24/gf32 не роутятся из-за ГЛУБИНЫ комбинационного облака (barrel-shift+sticky+CLZ+округление), НЕ таблицы -> split-table неприменим, нужна конвейеризация. - conformance/witness/gf_pipe/: независимый Fraction-оракул (само-проверка 200k vs numpy) + self-checking iverilog-стенд. gf24 30000/30000, gf32 30000/30000 bit-exact [verified SW на iverilog]. yosys generic synth OK (73 FF). P&R на плате = [routing-pending]. ТРЕК 3 (takum research): - research/takum_horizon_b_reassessment_2026-07-24.md: поправка честности - takum32/64 routing FAILURE = функциональный баг S1_R (split-table фиксит), НЕ routing-limit. Цепь 3.5/4 (CI+SHA256+UART есть, нет строки IDCODE). SSOT=83 не тронут. Синтез/прошивка/IDCODE = [ТРЕБУЕТ ДЕЙСТВИЯ ПОЛЬЗОВАТЕЛЯ].
1 parent 40f49fb commit d921d9e

7 files changed

Lines changed: 622 additions & 0 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vec_*.txt
2+
*.vvp
3+
__pycache__/
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# gf24/gf32 decode — pipelined variant + iverilog witness (horizon-B routing prep)
2+
3+
**Дата:** 2026-07-24. **Автор:** Vasilev (gHashTag), ORCID 0009-0008-4294-6159.
4+
**Статус:** `[verified SW на iverilog]` (функция). Synth/PnR/flash на AX7203 = `[ТРЕБУЕТ ДЕЙСТВИЯ ПОЛЬЗОВАТЕЛЯ]`.
5+
6+
## Зачем
7+
8+
gf24 (E9/M14/BIAS255) и gf32 (E12/M19/BIAS2047) decode **не проходят routing**
9+
на AX7203 (XC7A200T) при синтезе из чисто-комбинационного `gf_decode_param.v`
10+
(no-flatten CI = FAILURE, runs 28773511637 / 28773514467). Причина —
11+
**НЕ гигантская LUT-таблица** (в отличие от takum, который чинил split-BRAM),
12+
а **глубина одного комбинационного облака**: переменный barrel-shift (до ~40 бит)
13+
+ динамическая sticky-маска + CLZ + округление в одном `always@(*)`. Правильная
14+
horizon-B техника здесь — **конвейеризация датапата**, а не split таблицы.
15+
16+
## Что сделано
17+
18+
- `fpga/openxc7-synth/gf_decode_param_pipe.v` — 2-стадийный конвейер того же
19+
decode-закона (латентность 2 такта). Регистр вставлен ПОСЛЕ классификации +
20+
true_exp + вычисления shift, ПЕРЕД barrel-shift + округлением + FP32-pack.
21+
**Арифметика бит-в-бит идентична** `gf_decode_param.v` (те же iverilog-фиксы:
22+
widen-before-shift #1, `[23:0] sub_shifted` OOB-read #2). Изменена ТОЛЬКО
23+
временна́я структура, не функция.
24+
25+
## Пруф (независимый 2-й witness)
26+
27+
- `gf_decode_pipe_oracle.py` — golden Fraction-оракул gf{N}→FP32 (точный
28+
рациональный decode + RNE к binary32). **Структурно независим** от Verilog.
29+
Само-проверен: `oracle_selfcheck.py` = 200k fp32 round-trips бит-в-бит vs numpy.
30+
- `tb_gf_decode_param_pipe.v` — self-checking iverilog-стенд (по одному вектору
31+
с полным сбросом конвейера, без streaming-неоднозначности).
32+
33+
### Результаты (iverilog 12.0)
34+
35+
| Формат | Поля | Векторов | Результат |
36+
|---|---|---|---|
37+
| gf24 | E9/M14/BIAS255 | 30000 (repr.+5-class corners) | **30000/30000 bit-exact** |
38+
| gf32 | E12/M19/BIAS2047 | 30000 (repr.+5-class corners) | **30000/30000 bit-exact** |
39+
40+
Контроль: чисто-комбинационный `gf_decode_param.v` на тех же векторах = 30000/30000
41+
(оракул согласован и с оригиналом, и с numpy — тройная сверка).
42+
43+
yosys generic synth (`synth -flatten`, gf32): 1927 ячеек, 73 FF (2 стадии
44+
подтверждены). **Это `[смоделировано]`, НЕ P&R** — вердикт по routing даёт
45+
только openXC7 на плате.
46+
47+
## Воспроизвести
48+
49+
```bash
50+
cd conformance/witness/gf_pipe
51+
python3 oracle_selfcheck.py # оракул vs numpy
52+
python3 gf_decode_pipe_oracle.py --N 24 --E 9 --M 14 --BIAS 255 --count 30000 --out vec_gf24.txt
53+
python3 gf_decode_pipe_oracle.py --N 32 --E 12 --M 19 --BIAS 2047 --count 30000 --out vec_gf32.txt
54+
iverilog -g2012 -DN=24 -DE=9 -DM=14 -DBIAS=255 -DVEC='"vec_gf24.txt"' -DNVEC=30000 \
55+
-o s24.vvp ../../../fpga/openxc7-synth/gf_decode_param_pipe.v tb_gf_decode_param_pipe.v && vvp s24.vvp
56+
iverilog -g2012 -DN=32 -DE=12 -DM=19 -DBIAS=2047 -DVEC='"vec_gf32.txt"' -DNVEC=30000 \
57+
-o s32.vvp ../../../fpga/openxc7-synth/gf_decode_param_pipe.v tb_gf_decode_param_pipe.v && vvp s32.vvp
58+
```
59+
60+
## Честные границы
61+
62+
- Это **гипотеза** фикса routing: конвейер сокращает критический путь, но
63+
проходит ли gf24/gf32 P&R на Artix-7 — **проверяется ТОЛЬКО на плате**
64+
(openXC7 nextpnr-xilinx недоступен в песочнице). `[routing-pending]`.
65+
- iverilog доказывает **функцию** (encoding decode), НЕ Tier-E. Tier-E = полная
66+
цепь 4/4 (CI GREEN + SHA256 + UART N/N fails=0 @160000 + IDCODE 0x13636093).
67+
- Латентность выросла с 0 (комб.) / 1 (OUT_REG) до 2 тактов — host-скрипт
68+
UART-conformance должен учесть 2-тактовую задержку при чтении результата.
69+
- Представительная выборка (30k + 5-class corners), НЕ exhaustive (gf32 = 2³²
70+
недостижимо в песочнице; полный gf24 = 2²⁴ = 16.7M возможен на плате).
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""Golden Fraction oracle for gf{N} -> FP32 decode (independent of the RTL).
3+
4+
Emits iverilog test vectors: one line "GFHEX FP32HEX" per case. The RTL
5+
testbench (tb_gf_decode_param_pipe.v) reads these and compares bit-for-bit
6+
against gf_decode_param_pipe.v (latency=2). This oracle is a SECOND,
7+
structurally independent decode path (exact rational -> RNE to binary32),
8+
NOT a transcription of the Verilog -- satisfies the 2-witness honesty rule.
9+
10+
Trinity Catalog-100 horizon-B routing prep, 2026-07-24.
11+
Author: Vasilev (gHashTag), ORCID 0009-0008-4294-6159.
12+
Status: [verified SW на iverilog] once the tb passes.
13+
"""
14+
import sys, struct, argparse
15+
from fractions import Fraction
16+
17+
18+
def pow2(e):
19+
"""Exact Fraction 2^e for any integer e."""
20+
return Fraction(1 << e) if e >= 0 else Fraction(1, 1 << (-e))
21+
22+
23+
def fp32_bits_from_fraction(sign, frac):
24+
"""Round an exact non-negative Fraction to IEEE binary32, RNE. Return u32."""
25+
if frac == 0:
26+
return (sign << 31)
27+
# Find integer e with 2^e <= frac < 2^(e+1) using exact rational compare.
28+
num, den = frac.numerator, frac.denominator
29+
e = num.bit_length() - den.bit_length() # within +/-1 of the true value
30+
while frac < pow2(e):
31+
e -= 1
32+
while frac >= pow2(e + 1):
33+
e += 1
34+
35+
if e >= -126:
36+
# normal candidate: mantissa = frac/2^e in [1,2), scaled by 2^23
37+
mant_scaled = (frac / pow2(e)) * (1 << 23) # in [2^23, 2^24)
38+
q = round_half_even(mant_scaled)
39+
if q == (1 << 24): # rounded up to 2.0
40+
q = 1 << 23
41+
e += 1
42+
if e > 127:
43+
return (sign << 31) | 0x7F800000 # overflow -> inf
44+
mant = q - (1 << 23)
45+
biased = e + 127
46+
return (sign << 31) | (biased << 23) | mant
47+
else:
48+
# subnormal (or underflow): value in units of 2^-149
49+
units = frac / pow2(-149)
50+
q = round_half_even(units)
51+
if q >= (1 << 23): # rounded up to smallest normal
52+
return (sign << 31) | (1 << 23)
53+
return (sign << 31) | q
54+
55+
56+
def round_half_even(fr):
57+
"""Round a Fraction to nearest integer, ties to even."""
58+
fl = fr.numerator // fr.denominator
59+
rem = fr - fl
60+
if rem < Fraction(1, 2):
61+
return fl
62+
if rem > Fraction(1, 2):
63+
return fl + 1
64+
return fl if (fl % 2 == 0) else fl + 1
65+
66+
67+
def gf_decode_exact(word, N, E, M, BIAS):
68+
"""Independent exact decode: return ('nan'|'inf'|('val',sign,Fraction))."""
69+
sign = (word >> (N - 1)) & 1
70+
exp = (word >> M) & ((1 << E) - 1)
71+
mant = word & ((1 << M) - 1)
72+
exp_max = (1 << E) - 1
73+
if exp == exp_max and mant == 0:
74+
return ('inf', sign, None)
75+
if exp == exp_max and mant != 0:
76+
return ('nan', sign, None)
77+
if exp == 0 and mant == 0:
78+
return ('val', sign, Fraction(0))
79+
if exp == 0: # subnormal
80+
val = Fraction(mant, 1 << M) * (Fraction(2) ** (1 - BIAS))
81+
return ('val', sign, val)
82+
# normal
83+
val = (Fraction(1) + Fraction(mant, 1 << M)) * (Fraction(2) ** (exp - BIAS))
84+
return ('val', sign, val)
85+
86+
87+
def expected_fp32(word, N, E, M, BIAS):
88+
kind, sign, val = gf_decode_exact(word, N, E, M, BIAS)
89+
if kind == 'nan':
90+
return 0x7FC00001
91+
if kind == 'inf':
92+
return (sign << 31) | 0x7F800000
93+
return fp32_bits_from_fraction(sign, val)
94+
95+
96+
def main():
97+
ap = argparse.ArgumentParser()
98+
ap.add_argument("--N", type=int, required=True)
99+
ap.add_argument("--E", type=int, required=True)
100+
ap.add_argument("--M", type=int, required=True)
101+
ap.add_argument("--BIAS", type=int, required=True)
102+
ap.add_argument("--mode", choices=["exhaustive", "representative"], default="representative")
103+
ap.add_argument("--seed", type=int, default=20260724)
104+
ap.add_argument("--count", type=int, default=20000)
105+
ap.add_argument("--out", required=True)
106+
a = ap.parse_args()
107+
108+
words = []
109+
total = 1 << a.N
110+
if a.mode == "exhaustive" or total <= a.count:
111+
words = list(range(total))
112+
else:
113+
import random
114+
r = random.Random(a.seed)
115+
s = set()
116+
# always include the 5-class corners + all-exp boundaries
117+
for exp in [0, 1, (1 << a.E) - 2, (1 << a.E) - 1]:
118+
for mant in [0, 1, (1 << a.M) - 1, (1 << a.M) >> 1]:
119+
for sgn in [0, 1]:
120+
s.add((sgn << (a.N - 1)) | (exp << a.M) | (mant & ((1 << a.M) - 1)))
121+
while len(s) < a.count:
122+
s.add(r.getrandbits(a.N))
123+
words = sorted(s)
124+
125+
with open(a.out, "w") as f:
126+
for w in words:
127+
fp = expected_fp32(w, a.N, a.E, a.M, a.BIAS)
128+
f.write(f"{w:0{(a.N + 3) // 4}x} {fp:08x}\n")
129+
print(f"wrote {len(words)} vectors to {a.out} (N={a.N} E={a.E} M={a.M} BIAS={a.BIAS})")
130+
131+
132+
if __name__ == "__main__":
133+
main()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""Sanity: my Fraction->fp32 RNE matches numpy.float32 for random rationals
3+
that are within fp32 range. Confirms round_half_even + exponent logic."""
4+
import struct, random
5+
from fractions import Fraction
6+
from gf_decode_pipe_oracle import fp32_bits_from_fraction
7+
8+
import numpy as np
9+
10+
r = random.Random(1)
11+
bad = 0
12+
for _ in range(200000):
13+
# random fp32 pattern -> its exact value -> back through oracle
14+
u = r.getrandbits(32)
15+
# skip nan/inf/neg-zero specials; test finite positives/negatives
16+
exp = (u >> 23) & 0xFF
17+
if exp == 0xFF:
18+
continue
19+
f = struct.unpack("<f", struct.pack("<I", u))[0]
20+
if f == 0.0:
21+
continue
22+
sign = 1 if f < 0 else 0
23+
frac = Fraction(abs(f)) # exact value of the fp32
24+
got = fp32_bits_from_fraction(sign, frac)
25+
want = struct.unpack("<I", struct.pack("<f", f))[0]
26+
if got != want:
27+
bad += 1
28+
if bad <= 5:
29+
print(f"MISMATCH f={f!r} got={got:08x} want={want:08x}")
30+
if bad == 0:
31+
print("oracle_selfcheck: PASS (200k fp32 round-trips bit-exact)")
32+
else:
33+
print(f"oracle_selfcheck: FAIL ({bad} mismatches)")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// tb_gf_decode_param_pipe.v -- self-checking iverilog witness for the 2-stage
2+
// pipelined gf{N} decoder. Reads "GFHEX FP32HEX" vectors. For each vector it
3+
// applies gf_in, waits LAT_FLUSH clocks for the pipeline to fully propagate,
4+
// then samples fp32_out. This one-at-a-time settle avoids any streaming/
5+
// latency-alignment ambiguity in the checker (the RTL is still a real 2-stage
6+
// pipeline; we simply hold each input until its result emerges).
7+
//
8+
// Compile-time overrides: -DN=.. -DE=.. -DM=.. -DBIAS=.. -DVEC="file" -DNVEC=..
9+
// Trinity Catalog-100 horizon-B routing prep, 2026-07-24.
10+
`timescale 1ns/1ps
11+
`ifndef N
12+
`define N 24
13+
`endif
14+
`ifndef E
15+
`define E 9
16+
`endif
17+
`ifndef M
18+
`define M 14
19+
`endif
20+
`ifndef BIAS
21+
`define BIAS 255
22+
`endif
23+
`ifndef VEC
24+
`define VEC "vec_gf24.txt"
25+
`endif
26+
`ifndef NVEC
27+
`define NVEC 30000
28+
`endif
29+
30+
module tb;
31+
localparam integer N = `N, E = `E, M = `M, BIAS = `BIAS;
32+
localparam integer NV = `NVEC;
33+
localparam integer LAT_FLUSH = 4; // >= pipeline latency, margin included
34+
35+
reg clk = 0, rst_n = 0;
36+
reg [N-1:0] gf_in;
37+
wire [31:0] fp32_out;
38+
wire is_nan_o, is_inf_o, is_zero_o, is_subnormal_o;
39+
40+
gf_decode_param_pipe #(.N(N), .E(E), .M(M), .BIAS(BIAS)) dut (
41+
.clk(clk), .rst_n(rst_n), .gf_in(gf_in),
42+
.fp32_out(fp32_out), .is_nan_o(is_nan_o), .is_inf_o(is_inf_o),
43+
.is_zero_o(is_zero_o), .is_subnormal_o(is_subnormal_o));
44+
45+
always #5 clk = ~clk;
46+
47+
reg [N-1:0] vin [0:NV-1];
48+
reg [31:0] vexp [0:NV-1];
49+
integer nread = 0;
50+
integer fd, r, i, j;
51+
integer fails = 0, checked = 0;
52+
53+
function is_nan32; input [31:0] x;
54+
is_nan32 = (x[30:23] == 8'hFF) && (x[22:0] != 0);
55+
endfunction
56+
57+
initial begin
58+
fd = $fopen(`VEC, "r");
59+
if (fd == 0) begin $display("ERROR: cannot open %s", `VEC); $finish; end
60+
while (!$feof(fd) && nread < NV) begin
61+
r = $fscanf(fd, "%h %h\n", vin[nread], vexp[nread]);
62+
if (r == 2) nread = nread + 1;
63+
end
64+
$fclose(fd);
65+
$display("loaded %0d vectors from %s (N=%0d E=%0d M=%0d BIAS=%0d)",
66+
nread, `VEC, N, E, M, BIAS);
67+
68+
rst_n = 0; gf_in = 0;
69+
@(posedge clk); @(posedge clk);
70+
rst_n = 1;
71+
72+
for (i = 0; i < nread; i = i + 1) begin
73+
gf_in = vin[i];
74+
for (j = 0; j < LAT_FLUSH; j = j + 1) @(posedge clk);
75+
#1;
76+
if (is_nan32(vexp[i])) begin
77+
if (!is_nan32(fp32_out)) begin
78+
fails = fails + 1;
79+
if (fails <= 8) $display("FAIL idx=%0d gf=%h exp=NaN got=%h", i, vin[i], fp32_out);
80+
end
81+
end else if (fp32_out !== vexp[i]) begin
82+
fails = fails + 1;
83+
if (fails <= 8) $display("FAIL idx=%0d gf=%h exp=%h got=%h", i, vin[i], vexp[i], fp32_out);
84+
end
85+
checked = checked + 1;
86+
end
87+
88+
$display("WITNESS RESULT: %0d/%0d bit-exact (fails=%0d)", checked - fails, checked, fails);
89+
if (fails != 0) $finish(1);
90+
$finish(0);
91+
end
92+
endmodule

0 commit comments

Comments
 (0)