|
6 | 6 | import argparse |
7 | 7 | from pathlib import Path |
8 | 8 |
|
9 | | -from run_regression import corner_cases |
| 9 | +from run_regression import corner_cases, dot_product_int8 |
10 | 10 |
|
11 | 11 |
|
12 | 12 | ROOT = Path(__file__).resolve().parents[1] |
13 | 13 | OUTPUT = ROOT / "assets" / "rtl-regression.svg" |
| 14 | +SATURATION_OUTPUT = ROOT / "assets" / "rtl-saturation-evidence.svg" |
14 | 15 | SCENARIOS = ((1, 40), (8, 200), (17, 100)) |
15 | 16 |
|
16 | 17 |
|
@@ -94,21 +95,157 @@ def render() -> str: |
94 | 95 | ''' |
95 | 96 |
|
96 | 97 |
|
| 98 | +def render_saturation() -> str: |
| 99 | + cases = {case.name: case for case in corner_cases(vec_len=8)} |
| 100 | + names = ( |
| 101 | + "max_negative", |
| 102 | + "one_below_negative_limit", |
| 103 | + "exact_negative_limit", |
| 104 | + "exact_positive_limit", |
| 105 | + "one_above_positive_limit", |
| 106 | + "max_positive", |
| 107 | + ) |
| 108 | + expected = { |
| 109 | + "max_negative": (-130_048, -32_768), |
| 110 | + "one_below_negative_limit": (-32_769, -32_768), |
| 111 | + "exact_negative_limit": (-32_768, -32_768), |
| 112 | + "exact_positive_limit": (32_767, 32_767), |
| 113 | + "one_above_positive_limit": (32_768, 32_767), |
| 114 | + "max_positive": (129_032, 32_767), |
| 115 | + } |
| 116 | + labels = { |
| 117 | + "max_negative": "max negative", |
| 118 | + "one_below_negative_limit": "one below limit", |
| 119 | + "exact_negative_limit": "exact negative limit", |
| 120 | + "exact_positive_limit": "exact positive limit", |
| 121 | + "one_above_positive_limit": "one above limit", |
| 122 | + "max_positive": "max positive", |
| 123 | + } |
| 124 | + |
| 125 | + rows: list[tuple[str, int, int]] = [] |
| 126 | + for name in names: |
| 127 | + case = cases[name] |
| 128 | + exact_sum = sum(a * b for a, b in zip(case.vector_a, case.vector_b, strict=True)) |
| 129 | + emitted = dot_product_int8(case.vector_a, case.vector_b) |
| 130 | + if (exact_sum, emitted) != expected[name]: |
| 131 | + raise ValueError(f"named saturation vector drifted: {name}") |
| 132 | + rows.append((name, exact_sum, emitted)) |
| 133 | + |
| 134 | + plot_left = 104 |
| 135 | + plot_top = 154 |
| 136 | + plot_width = 676 |
| 137 | + plot_height = 350 |
| 138 | + exact_min = -140_000 |
| 139 | + exact_max = 140_000 |
| 140 | + output_min = -32_768 |
| 141 | + output_max = 32_767 |
| 142 | + |
| 143 | + def map_x(value: int) -> float: |
| 144 | + return plot_left + (value - exact_min) * plot_width / (exact_max - exact_min) |
| 145 | + |
| 146 | + def map_y(value: int) -> float: |
| 147 | + return plot_top + (output_max - value) * plot_height / (output_max - output_min) |
| 148 | + |
| 149 | + points = [] |
| 150 | + table_rows = [] |
| 151 | + for index, (name, exact_sum, emitted) in enumerate(rows): |
| 152 | + x = map_x(exact_sum) |
| 153 | + y = map_y(emitted) |
| 154 | + clamped = exact_sum != emitted |
| 155 | + points.append( |
| 156 | + f'''<circle cx="{x:.2f}" cy="{y:.2f}" r="8" fill="{"#e39a32" if clamped else "#2f827f"}" stroke="#fbf6ec" stroke-width="3"> |
| 157 | + <title>{labels[name]}: exact accumulator {exact_sum:,}, emitted {emitted:,}</title> |
| 158 | +</circle>''' |
| 159 | + ) |
| 160 | + row_y = 180 + index * 55 |
| 161 | + table_rows.append( |
| 162 | + f'''<text x="856" y="{row_y}" class="case">{labels[name]}</text> |
| 163 | +<text x="1160" y="{row_y}" text-anchor="end" class="numbers">{exact_sum:,} -> {emitted:,}</text> |
| 164 | +<text x="1190" y="{row_y}" class="{"clamp" if clamped else "exact"}">{"CLAMP" if clamped else "EXACT"}</text>''' |
| 165 | + ) |
| 166 | + |
| 167 | + lower_y = map_y(output_min) |
| 168 | + upper_y = map_y(output_max) |
| 169 | + lower_x = map_x(output_min) |
| 170 | + upper_x = map_x(output_max) |
| 171 | + zero_x = map_x(0) |
| 172 | + zero_y = map_y(0) |
| 173 | + |
| 174 | + return f'''<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="650" viewBox="0 0 1280 650" role="img" aria-labelledby="sat-title sat-desc"> |
| 175 | + <title id="sat-title">Edge AI RTL Lab signed 16-bit saturation evidence</title> |
| 176 | + <desc id="sat-desc">The clamp transfer curve and six named deterministic vectors show exact accumulator sums mapped to signed 16-bit outputs. Four vectors clamp and two remain exactly on the boundaries.</desc> |
| 177 | + <style> |
| 178 | + .title {{ font: 700 31px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #13253b; }} |
| 179 | + .subtitle,.axis,.footer {{ font: 15px system-ui, sans-serif; fill: #5f6974; }} |
| 180 | + .axis {{ font-size: 13px; }} |
| 181 | + .case {{ font: 600 14px system-ui, sans-serif; fill: #253c55; }} |
| 182 | + .numbers {{ font: 600 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #13253b; }} |
| 183 | + .clamp {{ font: 700 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #b66317; }} |
| 184 | + .exact {{ font: 700 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fill: #2f827f; }} |
| 185 | + .grid {{ stroke: #e5dac5; stroke-width: 1; }} |
| 186 | + .curve {{ fill: none; stroke: #203c5b; stroke-width: 5; stroke-linejoin: round; }} |
| 187 | + </style> |
| 188 | + <rect width="1280" height="650" rx="28" fill="#fbf6ec"/> |
| 189 | + <path d="M0 92H1280" stroke="#e6dac3"/> |
| 190 | + <text x="62" y="54" class="title">SIGNED SATURATION / NAMED RTL VECTORS</text> |
| 191 | + <text x="62" y="80" class="subtitle">Exact wide accumulator -> signed 16-bit output / VEC_LEN=8</text> |
| 192 | +
|
| 193 | + <g> |
| 194 | + <path d="M{plot_left} {upper_y:.2f}H{plot_left + plot_width}" class="grid"/> |
| 195 | + <path d="M{plot_left} {zero_y:.2f}H{plot_left + plot_width}" class="grid"/> |
| 196 | + <path d="M{plot_left} {lower_y:.2f}H{plot_left + plot_width}" class="grid"/> |
| 197 | + <path d="M{zero_x:.2f} {plot_top}V{plot_top + plot_height}" class="grid"/> |
| 198 | + <polyline points="{plot_left},{lower_y:.2f} {lower_x:.2f},{lower_y:.2f} {upper_x:.2f},{upper_y:.2f} {plot_left + plot_width},{upper_y:.2f}" class="curve"/> |
| 199 | + {"".join(points)} |
| 200 | + <text x="{plot_left - 16}" y="{upper_y + 5:.2f}" text-anchor="end" class="axis">32,767</text> |
| 201 | + <text x="{plot_left - 16}" y="{zero_y + 5:.2f}" text-anchor="end" class="axis">0</text> |
| 202 | + <text x="{plot_left - 16}" y="{lower_y + 5:.2f}" text-anchor="end" class="axis">-32,768</text> |
| 203 | + <text x="{plot_left}" y="536" text-anchor="middle" class="axis">-140k</text> |
| 204 | + <text x="{lower_x:.2f}" y="536" text-anchor="middle" class="axis">-32,768</text> |
| 205 | + <text x="{zero_x:.2f}" y="536" text-anchor="middle" class="axis">0</text> |
| 206 | + <text x="{upper_x:.2f}" y="536" text-anchor="middle" class="axis">32,767</text> |
| 207 | + <text x="{plot_left + plot_width}" y="536" text-anchor="middle" class="axis">140k</text> |
| 208 | + <text x="{plot_left + plot_width / 2}" y="570" text-anchor="middle" class="subtitle">exact accumulator sum</text> |
| 209 | + <text transform="translate(30 {plot_top + plot_height / 2}) rotate(-90)" text-anchor="middle" class="subtitle">emitted output</text> |
| 210 | + </g> |
| 211 | +
|
| 212 | + <path d="M824 132V516" stroke="#e6dac3"/> |
| 213 | + <text x="856" y="140" class="subtitle">named deterministic vectors</text> |
| 214 | + {"".join(table_rows)} |
| 215 | + <circle cx="868" cy="536" r="7" fill="#2f827f"/><text x="886" y="541" class="footer">exact boundary</text> |
| 216 | + <circle cx="1026" cy="536" r="7" fill="#e39a32"/><text x="1044" y="541" class="footer">saturated</text> |
| 217 | + <path d="M62 592H1218" stroke="#e6dac3"/> |
| 218 | + <text x="62" y="622" class="footer">Derived from corner_cases(8) and the bit-exact Python model; the same named vectors feed the self-checking RTL regression. No timing, area, or power claim.</text> |
| 219 | +</svg> |
| 220 | +''' |
| 221 | + |
| 222 | + |
97 | 223 | def main() -> None: |
98 | 224 | parser = argparse.ArgumentParser(description=__doc__) |
99 | 225 | parser.add_argument("--check", action="store_true", help="fail if the committed SVG is stale") |
100 | 226 | args = parser.parse_args() |
101 | | - expected = render() |
| 227 | + expected_outputs = { |
| 228 | + OUTPUT: render(), |
| 229 | + SATURATION_OUTPUT: render_saturation(), |
| 230 | + } |
102 | 231 |
|
103 | 232 | if args.check: |
104 | | - if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != expected: |
105 | | - raise SystemExit(f"stale generated asset: {OUTPUT.relative_to(ROOT)}") |
106 | | - print(f"up to date: {OUTPUT.relative_to(ROOT)}") |
| 233 | + stale = [ |
| 234 | + path |
| 235 | + for path, expected in expected_outputs.items() |
| 236 | + if not path.exists() or path.read_text(encoding="utf-8") != expected |
| 237 | + ] |
| 238 | + if stale: |
| 239 | + paths = ", ".join(str(path.relative_to(ROOT)) for path in stale) |
| 240 | + raise SystemExit(f"stale generated assets: {paths}") |
| 241 | + for path in expected_outputs: |
| 242 | + print(f"up to date: {path.relative_to(ROOT)}") |
107 | 243 | return |
108 | 244 |
|
109 | | - OUTPUT.parent.mkdir(parents=True, exist_ok=True) |
110 | | - OUTPUT.write_text(expected, encoding="utf-8") |
111 | | - print(f"wrote {OUTPUT.relative_to(ROOT)}") |
| 245 | + for path, expected in expected_outputs.items(): |
| 246 | + path.parent.mkdir(parents=True, exist_ok=True) |
| 247 | + path.write_text(expected, encoding="utf-8") |
| 248 | + print(f"wrote {path.relative_to(ROOT)}") |
112 | 249 |
|
113 | 250 |
|
114 | 251 | if __name__ == "__main__": |
|
0 commit comments