1use crate::codegen_sva::fabric_rtl::{
33 eval_fabric, fabric_to_bounded, residual_from_source, AluOp, FabricOp, Src,
34};
35use crate::codegen_sva::fanout::bounded_fanout_select;
36use crate::codegen_sva::mul_tree::{mul_tree, mul_tree_levels};
37use crate::codegen_sva::sva_to_verify::{BitVecBoundedOp, BoundedExpr};
38
39#[derive(Clone, Debug, PartialEq)]
43pub enum LaneArith {
44 Combinational,
47 DecomposedFma { stages: usize },
51 SequentialFma { select_slice: usize },
63 Streamed { every: usize },
69}
70
71#[derive(Clone, Debug)]
73pub struct SimtEngine {
74 pub n_lanes: usize,
76 pub n_in: usize,
78 pub uniform: Vec<bool>,
81 pub body: Vec<FabricOp>,
84 pub result: usize,
86 pub predicated: bool,
88 pub reduce: Option<AluOp>,
90 pub arith: LaneArith,
94}
95
96impl SimtEngine {
97 pub fn from_lane_source(
101 src: &str,
102 fn_name: &str,
103 n_lanes: usize,
104 uniform: Vec<bool>,
105 predicated: bool,
106 reduce: Option<AluOp>,
107 ) -> SimtEngine {
108 let (body, result, n_in) = residual_from_source(src, fn_name);
109 assert_eq!(
110 uniform.len(),
111 n_in,
112 "uniform flags ({}) must match kernel arity ({n_in})",
113 uniform.len()
114 );
115 SimtEngine {
116 n_lanes,
117 n_in,
118 uniform,
119 body,
120 result,
121 predicated,
122 reduce,
123 arith: LaneArith::SequentialFma { select_slice: 8 },
124 }
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq)]
132pub enum Slot {
133 Uniform(usize),
134 Mask,
135 Varying(usize, usize), }
137
138pub fn load_slots(eng: &SimtEngine) -> Vec<Slot> {
140 let mut s = Vec::new();
141 for k in 0..eng.n_in {
142 if eng.uniform[k] {
143 s.push(Slot::Uniform(k));
144 }
145 }
146 if eng.predicated {
147 s.push(Slot::Mask);
148 }
149 for j in 0..eng.n_lanes {
150 for k in 0..eng.n_in {
151 if !eng.uniform[k] {
152 s.push(Slot::Varying(j, k));
153 }
154 }
155 }
156 s
157}
158
159fn src_reg(uniform: &[bool], s: Src, j: usize) -> String {
163 match s {
164 Src::In(k) => {
165 if uniform[k] {
166 format!("u{k}")
167 } else {
168 format!("x{k}_l{j}")
169 }
170 }
171 Src::Val(k) => format!("v{k}_l{j}"),
172 Src::Const(c) => {
173 if c < 0 {
174 format!("-64'sd{}", (c as i128).unsigned_abs())
175 } else {
176 format!("64'sd{c}")
177 }
178 }
179 }
180}
181
182fn reduce_sep(op: AluOp) -> &'static str {
184 match op {
185 AluOp::Add => " + ",
186 AluOp::Sub => " - ",
187 AluOp::Mul => " * ",
188 AluOp::And => " & ",
189 AluOp::Or => " | ",
190 AluOp::Xor => " ^ ",
191 AluOp::Shl => " << ",
192 AluOp::Shr => " >>> ",
193 AluOp::UShr => " >> ",
194 }
195}
196
197pub fn emit_gpu_simt_verilog(name: &str, eng: &SimtEngine) -> String {
204 if let LaneArith::Streamed { every } = eng.arith {
205 return emit_gpu_simt_streamed_verilog(name, eng, every);
206 }
207 let n = eng.n_lanes;
208 const WIDTH: usize = 64;
213 let (p, is_seq, select_slice) = match &eng.arith {
217 LaneArith::Combinational => (0usize, false, 0usize),
218 LaneArith::DecomposedFma { stages } => (*stages, false, 0usize),
219 LaneArith::SequentialFma { select_slice } => (0usize, true, *select_slice),
220 LaneArith::Streamed { .. } => unreachable!("the streamed engine is emitted above"),
221 };
222 if is_seq {
223 assert_eq!(
224 eng.n_in, 3,
225 "the sequential (shift-add) FMA needs a 3-input a·x+b kernel"
226 );
227 assert!(
230 select_slice > 0,
231 "SequentialFma select_slice must be positive"
232 );
233 assert_eq!(
234 WIDTH % select_slice,
235 0,
236 "SequentialFma select_slice ({select_slice}) must divide the datapath width ({WIDTH}) evenly"
237 );
238 }
239 let slots = load_slots(eng);
240
241 let mut decls = String::new();
243 for k in 0..eng.n_in {
244 if eng.uniform[k] {
245 decls.push_str(&format!(" reg [63:0] u{k};\n"));
246 }
247 }
248 if eng.predicated {
249 decls.push_str(&format!(" reg [{}:0] mask;\n", n - 1));
250 }
251 for j in 0..n {
252 for k in 0..eng.n_in {
253 if !eng.uniform[k] {
254 decls.push_str(&format!(" reg [63:0] x{k}_l{j};\n"));
255 }
256 }
257 }
258 if p > 0 {
262 assert_eq!(
263 eng.n_in, 3,
264 "the pipelined (decomposed) FMA needs a 3-input a·x+b kernel"
265 );
266 for j in 0..n {
267 decls.push_str(&format!(
268 " reg [63:0] pll_l{j};\n reg [63:0] phl_l{j};\n reg [63:0] plh_l{j};\n"
269 ));
270 }
271 }
272 if is_seq {
276 for j in 0..n {
277 decls.push_str(&format!(
278 " reg [63:0] acc_l{j};\n reg [63:0] mc_l{j};\n reg [63:0] mp_l{j};\n"
279 ));
280 }
281 }
282 for j in 0..n {
283 decls.push_str(&format!(" reg [63:0] outr{j};\n"));
284 }
285 if eng.reduce.is_some() {
286 decls.push_str(" reg [63:0] redr;\n");
287 }
288 decls.push_str(" reg [15:0] lc;\n");
289 if p > 0 {
290 decls.push_str(" reg [7:0] pcnt;\n reg running;\n");
291 }
292 if is_seq {
293 decls.push_str(" reg [7:0] scnt;\n reg running;\n");
294 }
295
296 let mut lanes = String::new();
298 for j in 0..n {
299 if is_seq {
300 let (msel_decls, pp_expr) = bounded_fanout_select(
312 "msel",
313 &format!("_l{j}"),
314 &format!("mp_l{j}[0]"),
315 &format!("mc_l{j}"),
316 WIDTH,
317 select_slice,
318 );
319 lanes.push_str(&msel_decls);
320 lanes.push_str(&format!(" wire [{}:0] pp_l{j} = {pp_expr};\n", WIDTH - 1));
321 let b = src_reg(&eng.uniform, Src::In(2), j);
322 lanes.push_str(&format!(" wire [63:0] k_l{j} = acc_l{j} + {b};\n"));
323 let g = if eng.predicated {
324 format!("mask[{j}] ? k_l{j} : 64'sd0")
325 } else {
326 format!("k_l{j}")
327 };
328 lanes.push_str(&format!(" wire [63:0] g_l{j} = {g};\n"));
329 } else if p > 0 {
330 let a = src_reg(&eng.uniform, Src::In(0), j);
334 let x = src_reg(&eng.uniform, Src::In(1), j);
335 let b = src_reg(&eng.uniform, Src::In(2), j);
336 lanes.push_str(&format!(
337 " wire [31:0] al_l{j} = {a}[31:0]; wire [31:0] ah_l{j} = {a}[63:32];\n"
338 ));
339 lanes.push_str(&format!(
340 " wire [31:0] xl_l{j} = {x}[31:0]; wire [31:0] xh_l{j} = {x}[63:32];\n"
341 ));
342 lanes.push_str(&format!(
343 " wire [63:0] mid_l{j} = {{(phl_l{j}[31:0] + plh_l{j}[31:0]), 32'h0}};\n"
344 ));
345 lanes.push_str(&format!(" wire [63:0] ax_l{j} = pll_l{j} + mid_l{j};\n"));
346 lanes.push_str(&format!(" wire [63:0] k_l{j} = ax_l{j} + {b};\n"));
347 let g = if eng.predicated {
348 format!("mask[{j}] ? k_l{j} : 64'sd0")
349 } else {
350 format!("k_l{j}")
351 };
352 lanes.push_str(&format!(" wire [63:0] g_l{j} = {g};\n"));
353 } else {
354 for (i, op) in eng.body.iter().enumerate() {
355 lanes.push_str(&format!(
356 " wire [63:0] v{i}_l{j} = {};\n",
357 op.op.verilog(
358 &src_reg(&eng.uniform, op.a, j),
359 &src_reg(&eng.uniform, op.b, j)
360 )
361 ));
362 }
363 let kres = format!("v{}_l{j}", eng.result);
364 let g = if eng.predicated {
365 format!("mask[{j}] ? $signed({kres}) : 64'sd0")
366 } else {
367 format!("$signed({kres})")
368 };
369 lanes.push_str(&format!(" wire [63:0] g_l{j} = {g};\n"));
370 }
371 }
372 let reduce_decl = if let Some(op) = eng.reduce {
373 let terms: Vec<String> = (0..n).map(|j| format!("$signed(g_l{j})")).collect();
374 format!(" wire [63:0] creduced = {};\n", terms.join(reduce_sep(op)))
375 } else {
376 String::new()
377 };
378
379 let mut dout = String::new();
381 for j in 0..n {
382 dout.push_str(&format!("raddr == {j} ? outr{j} : "));
383 }
384 let dout_default = format!("outr{}", n - 1);
385 dout.push_str(if eng.reduce.is_some() {
386 "redr"
387 } else {
388 &dout_default
389 });
390
391 let mut load_case = String::new();
393 for (idx, slot) in slots.iter().enumerate() {
394 let lhs = match slot {
395 Slot::Uniform(k) => format!("u{k} <= din;"),
396 Slot::Mask => format!("mask <= din[{}:0];", n - 1),
397 Slot::Varying(j, k) => format!("x{k}_l{j} <= din;"),
398 };
399 load_case.push_str(&format!(" {idx}: {lhs}\n"));
400 }
401
402 let mut pipe_latch = String::new();
404 if p > 0 {
405 for j in 0..n {
406 pipe_latch.push_str(&format!(" pll_l{j} <= al_l{j} * xl_l{j};\n"));
407 pipe_latch.push_str(&format!(" phl_l{j} <= ah_l{j} * xl_l{j};\n"));
408 pipe_latch.push_str(&format!(" plh_l{j} <= al_l{j} * xh_l{j};\n"));
409 }
410 }
411
412 let mut compute = String::new();
414 for j in 0..n {
415 compute.push_str(&format!(" outr{j} <= g_l{j};\n"));
416 }
417 if eng.reduce.is_some() {
418 compute.push_str(" redr <= creduced;\n");
419 }
420
421 let mut seq_step = String::new();
425 if is_seq {
426 for j in 0..n {
427 seq_step.push_str(&format!(
428 "\x20\x20\x20\x20\x20\x20\x20\x20 acc_l{j} <= acc_l{j} + pp_l{j};\n\
429 \x20\x20\x20\x20\x20\x20\x20\x20 mc_l{j} <= mc_l{j} << 1;\n\
430 \x20\x20\x20\x20\x20\x20\x20\x20 mp_l{j} <= mp_l{j} >> 1;\n"
431 ));
432 }
433 }
434 let mut seq_load = String::new();
436 if is_seq {
437 for j in 0..n {
438 let a = src_reg(&eng.uniform, Src::In(0), j);
439 let x = src_reg(&eng.uniform, Src::In(1), j);
440 seq_load.push_str(&format!(
441 "\x20\x20\x20\x20\x20\x20\x20 acc_l{j} <= 64'd0; mc_l{j} <= {a}; mp_l{j} <= {x};\n"
442 ));
443 }
444 }
445
446 let fsm = if is_seq {
449 format!(
450 "\x20\x20\x20\x20\x20 if (go) begin running <= 1'b1; scnt <= 8'd{WIDTH}; done <= 1'b0;\n\
451 {seq_load}\
452 \x20\x20\x20\x20\x20 end\n\
453 \x20\x20\x20\x20\x20 else if (running) begin\n\
454 \x20\x20\x20\x20\x20\x20\x20 if (scnt > 8'd0) begin\n\
455 {seq_step}\
456 \x20\x20\x20\x20\x20\x20\x20\x20 scnt <= scnt - 8'd1;\n\
457 \x20\x20\x20\x20\x20\x20\x20 end else begin\n\
458 {compute}\
459 \x20\x20\x20\x20\x20\x20\x20\x20 done <= 1'b1; running <= 1'b0;\n\
460 \x20\x20\x20\x20\x20\x20\x20 end\n\
461 \x20\x20\x20\x20\x20 end\n"
462 )
463 } else if p > 0 {
464 format!(
465 "\x20\x20\x20\x20\x20 if (go) begin running <= 1'b1; pcnt <= 8'd{p}; done <= 1'b0; end\n\
466 \x20\x20\x20\x20\x20 else if (running) begin\n\
467 \x20\x20\x20\x20\x20\x20\x20 if (pcnt > 8'd1) pcnt <= pcnt - 8'd1;\n\
468 \x20\x20\x20\x20\x20\x20\x20 else begin\n\
469 {compute}\
470 \x20\x20\x20\x20\x20\x20\x20\x20 done <= 1'b1; running <= 1'b0;\n\
471 \x20\x20\x20\x20\x20\x20\x20 end\n\
472 \x20\x20\x20\x20\x20 end\n"
473 )
474 } else {
475 format!(
476 "\x20\x20\x20\x20\x20 if (go) begin\n\
477 {compute}\
478 \x20\x20\x20\x20\x20\x20\x20 done <= 1'b1;\n\
479 \x20\x20\x20\x20\x20 end\n"
480 )
481 };
482 let rst_extra = if p > 0 || is_seq {
483 " running <= 1'b0;"
484 } else {
485 ""
486 };
487 let pipe_note = if is_seq {
488 format!(" The per-lane FMA is a SEQUENTIAL shift-add over {WIDTH} bits (one reused adder per lane, bounded fan-out → antenna-trivial); a result is ready {} cycles after `go` (done pulses).", WIDTH + 1)
489 } else if p > 0 {
490 format!(" The per-lane FMA is PIPELINED {p} stages (real GPU-ALU pipelining); a result is ready `pipe` cycles after `go` (done pulses).")
491 } else {
492 String::new()
493 };
494
495 let pred_note = if eng.predicated {
496 ", gated by a per-lane mask (SIMT divergence)"
497 } else {
498 ""
499 };
500 let red_note = if eng.reduce.is_some() {
501 ", then a cross-lane warp-reduce (raddr==N)"
502 } else {
503 ""
504 };
505 format!(
506 "// gpu_{name} — a tuneable SIMT data-parallel engine: {n} spatial lanes, each running the\n\
507 // SAME compiled straight-line kernel (the shader) on its own data{pred_note}{red_note}.\n\
508 // The lanes are WIRED (spatial) — no warp scheduler, no instruction fetch/decode/opcode\n\
509 // dispatch.{pipe_note} Narrow streaming I/O: `din` is loaded slot-by-slot into the lane\n\
510 // register file, `raddr` reads results back. SIMD = all lanes unmasked; map-then-reduce\n\
511 // feeds the NPU accumulator. 64-bit, wrapping like the VM's Int; proven ≡ the VM by our SAT.\n\
512 module gpu_{name}(input clk, input rst, input load, input [63:0] din,\n\
513 \x20 input go, input [7:0] raddr, output [63:0] dout, output reg done);\n\
514 {decls}{lanes}{reduce_decl} assign dout = {dout};\n\
515 \x20 always @(posedge clk) begin\n\
516 {pipe_latch}\
517 \x20\x20\x20 if (rst) begin\n\
518 \x20\x20\x20\x20\x20 lc <= 16'd0; done <= 1'b0;{rst_extra}\n\
519 \x20\x20\x20 end else begin\n\
520 \x20\x20\x20\x20\x20 if (load) begin\n\
521 \x20\x20\x20\x20\x20\x20\x20 case (lc)\n\
522 {load_case}\
523 \x20\x20\x20\x20\x20\x20\x20\x20 default: ;\n\
524 \x20\x20\x20\x20\x20\x20\x20 endcase\n\
525 \x20\x20\x20\x20\x20\x20\x20 lc <= lc + 16'd1;\n\
526 \x20\x20\x20\x20\x20 end\n\
527 {fsm}\
528 \x20\x20\x20 end\n\
529 \x20 end\n\
530 endmodule\n"
531 )
532}
533
534pub(crate) fn computes_affine_fma(eng: &SimtEngine) -> bool {
536 let words = [0i64, 1, -1, 2, i64::MAX, i64::MIN, 0x5555_5555_5555_5555, -0x1234_5678_9ABC_DEF1, 1 << 32, 3];
537 eng.n_in == 3
538 && words.iter().all(|&a| {
539 words.iter().all(|&x| {
540 let b = a.rotate_left(7) ^ x;
541 eval_fabric(&eng.body, &[a, x, b], eng.result) == a.wrapping_mul(x).wrapping_add(b)
542 })
543 })
544}
545
546fn emit_gpu_simt_streamed_verilog(name: &str, eng: &SimtEngine, every: usize) -> String {
549 let n = eng.n_lanes;
550 assert!(every >= 1, "a streamed multiply registers after every `every`-th level, every >= 1");
551 assert!(computes_affine_fma(eng), "the streamed multiply serves the a·x + b kernel");
552 assert!(!eng.uniform[1], "the streamed multiply issues a lane's a·x when its x loads: x is per-lane");
553 let (identity, fold) = match eng.reduce {
554 None => ("64'd0", ""),
555 Some(AluOp::Add) => ("64'd0", "redr + g"),
556 Some(AluOp::Or) => ("64'd0", "redr | g"),
557 Some(AluOp::Xor) => ("64'd0", "redr ^ g"),
558 Some(AluOp::And) => ("{64{1'b1}}", "redr & g"),
559 Some(op) => panic!("the streamed engine folds a warp-reduce of Add, Or, Xor or And, not {op:?}"),
560 };
561 let levels = mul_tree_levels(64);
562 let tree = mul_tree("mt", "ma", "mx", 64, |l| (l + 1) % every == 0 || l + 1 == levels);
563 let lat = tree.latency as usize;
564 let tag = (usize::BITS - (n.max(2) - 1).leading_zeros()) as usize;
565 let a_of = |j: usize| src_reg(&eng.uniform, Src::In(0), j);
566 let b_of = |j: usize| src_reg(&eng.uniform, Src::In(2), j);
567
568 let mut decls = String::new();
569 for k in [0usize, 2] {
570 if eng.uniform[k] {
571 decls.push_str(&format!(" reg [63:0] u{k};\n"));
572 } else {
573 for j in 0..n {
574 decls.push_str(&format!(" reg [63:0] x{k}_l{j};\n"));
575 }
576 }
577 }
578 if eng.predicated {
579 decls.push_str(&format!(" reg [{}:0] mask;\n", n - 1));
580 }
581 decls.push_str(&format!(" reg [63:0] ma, mx; reg mv; reg [{}:0] ml;\n", tag - 1));
582 for (r, bits) in &tree.regs {
583 decls.push_str(&format!(" reg [{}:0] {r};\n", bits - 1));
584 }
585 decls.push_str(&format!(" reg pv [1:{lat}]; reg [{}:0] pl [1:{lat}];\n", tag - 1));
586 for j in 0..n {
587 decls.push_str(&format!(" reg [63:0] outr{j};\n"));
588 }
589 decls.push_str(" reg [63:0] g, redr; reg gv, waiting; reg [15:0] lc; reg [7:0] nd;\n");
590
591 let lane = format!("pl[{lat}]");
592 let b_lane = if eng.uniform[2] {
593 "u2".to_string()
594 } else {
595 (0..n).rev().fold(String::from("64'd0"), |acc, j| format!("{lane} == {j} ? {} : {acc}", b_of(j)))
596 };
597 let mut wires = tree.wires.clone();
598 wires.push_str(&format!(" wire [63:0] k_out = {} + ({b_lane});\n", tree.product));
599 let kept = if eng.predicated { format!("mask[{lane}] ? k_out : 64'd0") } else { "k_out".to_string() };
600 wires.push_str(&format!(" wire [63:0] g_out = {kept};\n"));
601
602 let mut load_case = String::new();
603 for (idx, slot) in load_slots(eng).iter().enumerate() {
604 let lhs = match *slot {
605 Slot::Uniform(k) => format!("u{k} <= din;"),
606 Slot::Mask => format!("mask <= din[{}:0];", n - 1),
607 Slot::Varying(j, 1) => format!("begin ma <= {}; mx <= din; mv <= 1'b1; ml <= {tag}'d{j}; end", a_of(j)),
608 Slot::Varying(j, k) => format!("x{k}_l{j} <= din;"),
609 };
610 load_case.push_str(&format!(" {idx}: {lhs}\n"));
611 }
612 let moves: String = tree.moves.iter().map(|(r, e)| format!(" {r} <= {e};\n")).collect();
613 let mut lanes_out = String::new();
614 for j in 0..n {
615 lanes_out.push_str(&format!(" if ({lane} == {tag}'d{j}) outr{j} <= g_out;\n"));
616 }
617 let shift: String = (2..=lat).map(|k| format!(" pv[{k}] <= pv[{}]; pl[{k}] <= pl[{}];\n", k - 1, k - 1)).collect();
618 let mut dout = String::new();
619 for j in 0..n {
620 dout.push_str(&format!("raddr == {j} ? outr{j} : "));
621 }
622 dout.push_str(if eng.reduce.is_some() { "redr" } else { "64'd0" });
623 let fold_line = if eng.reduce.is_some() { format!(" if (gv) redr <= {fold};\n") } else { String::new() };
624 format!(
625 "// gpu_{name} — a SIMT engine of {n} lanes sharing one pipelined multiply (an aligned tree of adds, {lat} \
626 register stages):\n\
627 // each lane's a·x enters it in the clock its x loads, and its masked a·x + b is folded into the warp-reduce \
628 as it emerges.\n\
629 // 64-bit, wrapping like the VM's Int.\n\
630 module gpu_{name}(input clk, input rst, input load, input [63:0] din,\n\
631 \x20 input go, input [7:0] raddr, output [63:0] dout, output reg done);\n\
632 {decls}{wires} assign dout = {dout};\n\
633 \x20 always @(posedge clk) begin\n\
634 {moves}\x20 pv[1] <= mv; pl[1] <= ml;\n\
635 {shift}\x20 if (rst) begin\n\
636 \x20 lc <= 16'd0; done <= 1'b0; waiting <= 1'b0; nd <= 8'd0; mv <= 1'b0; gv <= 1'b0; redr <= {identity};\n\
637 \x20 end else begin\n\
638 \x20 mv <= 1'b0;\n\
639 \x20 if (load) begin\n\
640 \x20 case (lc)\n\
641 {load_case}\
642 \x20 default: ;\n\
643 \x20 endcase\n\
644 \x20 lc <= lc + 16'd1;\n\
645 \x20 end\n\
646 \x20 gv <= pv[{lat}];\n\
647 \x20 if (pv[{lat}]) begin\n\
648 \x20 g <= g_out;\n\
649 {lanes_out}\
650 \x20 end\n\
651 {fold_line}\
652 \x20 if (gv) nd <= nd + 8'd1;\n\
653 \x20 if (go) waiting <= 1'b1;\n\
654 \x20 if ((go || waiting) && nd == 8'd{n}) begin done <= 1'b1; waiting <= 1'b0; end\n\
655 \x20 end\n\
656 \x20 end\n\
657 endmodule\n"
658 )
659}
660
661pub fn eval_gpu_simt(
666 eng: &SimtEngine,
667 lane_inputs: &[Vec<i64>],
668 mask: &[bool],
669) -> (Vec<i64>, Option<i64>) {
670 assert_eq!(lane_inputs.len(), eng.n_lanes, "one input vector per lane");
671 if eng.predicated {
672 assert_eq!(mask.len(), eng.n_lanes, "one mask bit per lane");
673 }
674 let mut outs = Vec::with_capacity(eng.n_lanes);
675 for (j, li) in lane_inputs.iter().enumerate() {
676 assert_eq!(li.len(), eng.n_in, "lane {j} needs {} inputs", eng.n_in);
677 let k = eval_fabric(&eng.body, li, eng.result);
678 let active = !eng.predicated || mask[j];
679 outs.push(if active { k } else { 0 });
680 }
681 let reduced = eng.reduce.map(|op| {
682 let seed = match op {
684 AluOp::Add
685 | AluOp::Sub
686 | AluOp::Or
687 | AluOp::Xor
688 | AluOp::Shl
689 | AluOp::Shr
690 | AluOp::UShr => 0,
691 AluOp::Mul => 1,
692 AluOp::And => -1, };
694 outs.iter().copied().fold(seed, |acc, x| op.eval(acc, x))
695 });
696 (outs, reduced)
697}
698
699pub fn simt_masked_lane_to_bounded(eng: &SimtEngine, width: u32) -> BoundedExpr {
703 let kernel = fabric_to_bounded(&eng.body, eng.result, width);
704 let cond = BoundedExpr::BitVecBinary {
705 op: BitVecBoundedOp::Eq,
706 left: Box::new(BoundedExpr::BitVecVar("mask".to_string(), width)),
707 right: Box::new(BoundedExpr::BitVecConst { width, value: 1 }),
708 };
709 BoundedExpr::BitVecIte {
710 cond: Box::new(cond),
711 then: Box::new(kernel),
712 else_: Box::new(BoundedExpr::BitVecConst { width, value: 0 }),
713 }
714}
715
716pub fn simt_reduce_to_bounded(eng: &SimtEngine, op: AluOp, width: u32) -> BoundedExpr {
720 let bop = match op {
721 AluOp::Add => BitVecBoundedOp::Add,
722 AluOp::Sub => BitVecBoundedOp::Sub,
723 AluOp::Mul => BitVecBoundedOp::Mul,
724 AluOp::And => BitVecBoundedOp::And,
725 AluOp::Or => BitVecBoundedOp::Or,
726 AluOp::Xor => BitVecBoundedOp::Xor,
727 AluOp::Shl => BitVecBoundedOp::Shl,
728 AluOp::Shr => BitVecBoundedOp::AShr,
729 AluOp::UShr => BitVecBoundedOp::Shr,
730 };
731 let mut acc = BoundedExpr::BitVecVar("out0".to_string(), width);
732 for j in 1..eng.n_lanes {
733 acc = BoundedExpr::BitVecBinary {
734 op: bop,
735 left: Box::new(acc),
736 right: Box::new(BoundedExpr::BitVecVar(format!("out{j}"), width)),
737 };
738 }
739 acc
740}
741
742pub fn simt_sequential_fma_to_bounded(width: u32) -> BoundedExpr {
749 let a = BoundedExpr::BitVecVar("in0".to_string(), width);
750 let x = BoundedExpr::BitVecVar("in1".to_string(), width);
751 let mut acc = BoundedExpr::BitVecConst { width, value: 0 };
752 for i in 0..width {
753 let xi = BoundedExpr::BitVecExtract {
755 high: i,
756 low: i,
757 operand: Box::new(x.clone()),
758 };
759 let cond = BoundedExpr::BitVecBinary {
760 op: BitVecBoundedOp::Eq,
761 left: Box::new(xi),
762 right: Box::new(BoundedExpr::BitVecConst { width: 1, value: 1 }),
763 };
764 let shifted = BoundedExpr::BitVecBinary {
766 op: BitVecBoundedOp::Shl,
767 left: Box::new(a.clone()),
768 right: Box::new(BoundedExpr::BitVecConst {
769 width,
770 value: i as u64,
771 }),
772 };
773 let term = BoundedExpr::BitVecIte {
774 cond: Box::new(cond),
775 then: Box::new(shifted),
776 else_: Box::new(BoundedExpr::BitVecConst { width, value: 0 }),
777 };
778 acc = BoundedExpr::BitVecBinary {
779 op: BitVecBoundedOp::Add,
780 left: Box::new(acc),
781 right: Box::new(term),
782 };
783 }
784 acc
785}
786
787pub fn simt_sliced_pp_to_bounded(select_slice: usize, width: u32) -> BoundedExpr {
799 assert!(select_slice > 0, "select_slice must be positive");
800 assert_eq!(
801 width as usize % select_slice,
802 0,
803 "select_slice ({select_slice}) must divide width ({width}) evenly"
804 );
805 let group = width / select_slice as u32;
806 let s = BoundedExpr::BitVecVar("mp0".to_string(), 1);
807 let mc = BoundedExpr::BitVecVar("mc".to_string(), width);
808 let cond = BoundedExpr::BitVecBinary {
810 op: BitVecBoundedOp::Eq,
811 left: Box::new(s),
812 right: Box::new(BoundedExpr::BitVecConst { width: 1, value: 1 }),
813 };
814 let group_pp = |g: u32| -> BoundedExpr {
816 let hi = (g + 1) * group - 1;
817 let lo = g * group;
818 BoundedExpr::BitVecIte {
819 cond: Box::new(cond.clone()),
820 then: Box::new(BoundedExpr::BitVecExtract {
821 high: hi,
822 low: lo,
823 operand: Box::new(mc.clone()),
824 }),
825 else_: Box::new(BoundedExpr::BitVecConst {
826 width: group,
827 value: 0,
828 }),
829 }
830 };
831 let top = select_slice as u32 - 1;
833 let mut pp = group_pp(top);
834 for g in (0..top).rev() {
835 pp = BoundedExpr::BitVecConcat(Box::new(pp), Box::new(group_pp(g)));
836 }
837 pp
838}
839
840pub fn signoff_advisory(arith: &LaneArith, width: u32, n_lanes: usize) -> Option<String> {
846 let dense = matches!(
847 arith,
848 LaneArith::Combinational | LaneArith::DecomposedFma { .. }
849 );
850 if dense && width >= 32 {
851 Some(format!(
852 "signoff advisory: {arith:?} at width {width} across {n_lanes} lane(s) is a WIDE \
853 combinational multiply — met1-antenna-dense on Sky130 (expect met1 area-ratio \
854 violations even at low utilization). Consider LaneArith::SequentialFma (one reused \
855 adder per lane, bounded fan-out) for an antenna-trivial signoff. This is advisory \
856 only — emission is unaffected."
857 ))
858 } else {
859 None
860 }
861}
862
863fn slot_value(slot: Slot, lane_inputs: &[Vec<i64>], mask: &[bool]) -> u64 {
865 match slot {
866 Slot::Uniform(k) => lane_inputs[0][k] as u64,
867 Slot::Mask => {
868 let mut m: u64 = 0;
869 for (j, &b) in mask.iter().enumerate() {
870 if b {
871 m |= 1 << j;
872 }
873 }
874 m
875 }
876 Slot::Varying(j, k) => lane_inputs[j][k] as u64,
877 }
878}
879
880pub fn write_gpu_hex(eng: &SimtEngine, vectors: &[(Vec<Vec<i64>>, Vec<bool>)]) -> (String, String) {
886 let slots = load_slots(eng);
887 let mut hin = String::new();
888 let mut hout = String::new();
889 for (lane_inputs, mask) in vectors {
890 for &slot in &slots {
891 hin.push_str(&format!("{:016x}\n", slot_value(slot, lane_inputs, mask)));
892 }
893 let (outs, reduced) = eval_gpu_simt(eng, lane_inputs, mask);
894 for o in &outs {
895 hout.push_str(&format!("{:016x}\n", *o as u64));
896 }
897 if let Some(r) = reduced {
898 hout.push_str(&format!("{:016x}\n", r as u64));
899 }
900 }
901 (hin, hout)
902}
903
904fn inputs_per_vector(eng: &SimtEngine) -> usize {
906 load_slots(eng).len()
907}
908
909fn outputs_per_vector(eng: &SimtEngine) -> usize {
911 eng.n_lanes + if eng.reduce.is_some() { 1 } else { 0 }
912}
913
914pub fn emit_gpu_simt_testbench(name: &str, eng: &SimtEngine, n_vectors: usize) -> String {
919 let ipv = inputs_per_vector(eng);
920 let opv = outputs_per_vector(eng);
921 let in_last = n_vectors * ipv - 1;
922 let out_last = n_vectors * opv - 1;
923 let n_reads = opv; let wait_cycles = match &eng.arith {
928 LaneArith::Combinational => 0,
929 LaneArith::DecomposedFma { stages } => *stages,
930 LaneArith::SequentialFma { .. } => 65,
931 LaneArith::Streamed { .. } => 0,
932 };
933 let pipe_wait = if matches!(eng.arith, LaneArith::Streamed { .. }) {
934 "while (!done) begin @(posedge clk); #1; end ".to_string()
935 } else if wait_cycles > 0 {
936 format!("repeat ({wait_cycles}) @(posedge clk); ")
937 } else {
938 String::new()
939 };
940 format!(
941 "// tb_gpu_{name} — the SIMT engine's N-lane map (+ warp-reduce) vs the RUNNING VM's output.\n\
942 `timescale 1ns/1ps\n\
943 module tb_gpu_{name};\n\
944 \x20 reg clk = 0; reg rst, load, go; reg [63:0] din; reg [7:0] raddr;\n\
945 \x20 wire [63:0] dout; wire done;\n\
946 \x20 reg [63:0] oin [0:{in_last}];\n\
947 \x20 reg [63:0] oout [0:{out_last}];\n\
948 \x20 integer i, s, r, errors = 0;\n\
949 \x20 gpu_{name} dut(.clk(clk), .rst(rst), .load(load), .din(din), .go(go),\n\
950 \x20 .raddr(raddr), .dout(dout), .done(done));\n\
951 \x20 always #5 clk = ~clk;\n\
952 \x20 initial begin\n\
953 \x20\x20\x20 $readmemh(\"gpu_{name}_in.hex\", oin);\n\
954 \x20\x20\x20 $readmemh(\"gpu_{name}_out.hex\", oout);\n\
955 \x20\x20\x20 rst = 1; load = 0; go = 0; din = 0; raddr = 0;\n\
956 \x20\x20\x20 for (i = 0; i < {n_vectors}; i = i + 1) begin\n\
957 \x20\x20\x20\x20\x20 rst = 1; @(posedge clk); #1 rst = 0;\n\
958 \x20\x20\x20\x20\x20 for (s = 0; s < {ipv}; s = s + 1) begin\n\
959 \x20\x20\x20\x20\x20\x20\x20 din = oin[i*{ipv}+s]; load = 1; @(posedge clk); #1;\n\
960 \x20\x20\x20\x20\x20 end\n\
961 \x20\x20\x20\x20\x20 load = 0; go = 1; @(posedge clk); #1 go = 0; {pipe_wait}\n\
962 \x20\x20\x20\x20\x20 for (r = 0; r < {n_reads}; r = r + 1) begin\n\
963 \x20\x20\x20\x20\x20\x20\x20 raddr = r; #1;\n\
964 \x20\x20\x20\x20\x20\x20\x20 if (dout !== oout[i*{opv}+r]) begin errors = errors + 1; $display(\"FAIL vec %0d read %0d\", i, r); end\n\
965 \x20\x20\x20\x20\x20 end\n\
966 \x20\x20\x20 end\n\
967 \x20\x20\x20 if (errors == 0) $display(\"PASS: {n_vectors} SIMT evaluations match the VM (N-lane map, no warp scheduler)\");\n\
968 \x20\x20\x20 else $display(\"FAIL: %0d SIMT mismatches\", errors);\n\
969 \x20\x20\x20 $finish;\n\
970 \x20 end\n\
971 endmodule\n"
972 )
973}