CharlesCNorton commited on
Commit
22baf26
·
1 Parent(s): f6bd63a

rv32: FCVT.W.S honors the instruction rounding-mode field (RNE/RTZ/RDN/RUP/RMM), verified against an exact rational reference across all modes

Browse files
Files changed (2) hide show
  1. src/machines.py +158 -26
  2. todo.md +0 -1
src/machines.py CHANGED
@@ -222,22 +222,48 @@ def fcvt_s_w_ref(x: int) -> int:
222
  return struct.unpack("<I", struct.pack("<f", float(xi)))[0]
223
 
224
 
225
- def fcvt_w_s_ref(w: int) -> int:
226
- """float32 word -> signed int32, round toward zero (RISC-V rtz),
227
- saturating; NaN -> INT_MAX, infinities -> INT_MIN/INT_MAX."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
229
  if e == 0xFF:
230
  return 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
231
- E = e - 127
232
- if E < 0:
233
- return 0
234
- mant = (1 << 23) | f
235
- if E >= 31:
236
  if s and E == 31 and f == 0:
237
- return 0x80000000
238
  return 0x80000000 if s else 0x7FFFFFFF
239
- mag = (mant << (E - 23)) if E >= 23 else (mant >> (23 - E))
240
- return ((-mag) & MASK32) if s else (mag & MASK32)
 
 
 
 
 
 
 
 
 
241
 
242
 
243
  def sext(v: int, bits: int) -> int:
@@ -343,7 +369,8 @@ class Asm:
343
  if mn == "fmv.x.w":
344
  return (0x70 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
345
  if mn == "fcvt.w.s":
346
- return (0x60 << 25) | (a[1] << 15) | (1 << 12) | (a[0] << 7) | 0x53 # rm=rtz
 
347
  if mn == "fcvt.s.w":
348
  return (0x68 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
349
  if mn in ("fsgnj.s", "fsgnjn.s", "fsgnjx.s"):
@@ -498,7 +525,7 @@ class RefRV32:
498
  from eval import float_bits_to_value
499
  x, y = float_bits_to_value(fa, 8, 23), float_bits_to_value(fb, 8, 23)
500
  wr(1 if [x <= y, x < y, x == y][f3] else 0)
501
- elif f7 == 0x60: wr(fcvt_w_s_ref(FR[rs1])) # FCVT.W.S
502
  elif f7 == 0x68: FR[rd] = fcvt_s_w_ref(R[rs1]) # FCVT.S.W
503
  elif f7 == 0x78: FR[rd] = a
504
  elif f7 == 0x70: wr(FR[rs1])
@@ -749,18 +776,15 @@ class Rv32ThresholdCPU:
749
  exp += 1
750
  return (s << 31) | (exp << 23) | frac
751
 
752
- def fcvt_w_s(self, w):
753
- """float32 -> signed int32, round toward zero (RISC-V rtz), saturating.
754
- NaN -> INT_MAX; overflow saturates to INT_MIN/INT_MAX."""
 
755
  s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
756
  if e == 0xFF:
757
- if f:
758
- return 0x7FFFFFFF # NaN
759
- return 0x80000000 if s else 0x7FFFFFFF # +-inf
760
- E = e - 127
761
- if E < 0:
762
- return 0
763
- mant = (1 << 23) | f
764
  if E >= 31:
765
  if s and E == 31 and f == 0:
766
  return 0x80000000 # exactly -2^31
@@ -768,8 +792,14 @@ class Rv32ThresholdCPU:
768
  if E >= 23:
769
  mag = self.sll(mant, E - 23)
770
  else:
771
- mag = self.srl(mant, 23 - E)
772
- return self.neg32(mag) if s else (mag & MASK32)
 
 
 
 
 
 
773
 
774
  def neur(self, x_reg, w_reg):
775
  x = int_to_bits(x_reg & 0xFF, 8) # MSB-first bits of low byte
@@ -1043,7 +1073,7 @@ class Rv32ThresholdCPU:
1043
  s_out = self._bitwise("xor", sa << 31, sb << 31) >> 31 # FSGNJX.S
1044
  FR[rd] = (FR[rs1] & 0x7FFFFFFF) | (s_out << 31)
1045
  elif f7 == 0x60: # FCVT.W.S: float -> signed int, gate-routed
1046
- wr(self.fcvt_w_s(FR[rs1]))
1047
  elif f7 == 0x68: # FCVT.S.W: signed int -> float, gate-routed
1048
  FR[rd] = self.fcvt_s_w(R[rs1])
1049
  elif f7 == 0x78:
@@ -1553,6 +1583,33 @@ def prog_fcvt():
1553
  return mem, 30, lambda s: s["regs"][3] == 8 and s["regs"][5] == (-5 & 0xFFFFFFFF)
1554
 
1555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1556
  def rv32_netlist_check() -> bool:
1557
  """Verify the rv32-specific sub-circuits (NEUR, hazard, MULHU accumulator)
1558
  are recoverable and correct from the shipped .inputs metadata alone."""
@@ -1616,6 +1673,79 @@ def rv32_netlist_check() -> bool:
1616
  return ok
1617
 
1618
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1619
  def rv32_test() -> int:
1620
  print("Loading neural_rv32...")
1621
  cpu = Rv32ThresholdCPU()
@@ -1627,6 +1757,7 @@ def rv32_test() -> int:
1627
  ("muldiv", prog_muldiv),
1628
  ("float_subset", prog_float),
1629
  ("fcvt_roundtrip", prog_fcvt),
 
1630
  ("neur_xor_net", prog_neur),
1631
  ("mmio_print", prog_mmio),
1632
  ]
@@ -1635,6 +1766,7 @@ def rv32_test() -> int:
1635
  mem, budget, check = builder()
1636
  all_ok &= lockstep(cpu, mem, budget, name, check)
1637
  all_ok &= rv32_netlist_check()
 
1638
  print(f"dual-issue pairs retired: {cpu.pairs_issued}")
1639
  print("RV32:", "PASS" if all_ok else "FAIL")
1640
  return 0 if all_ok else 1
 
222
  return struct.unpack("<I", struct.pack("<f", float(xi)))[0]
223
 
224
 
225
+ RM_NAMES = {"rne": 0, "rtz": 1, "rdn": 2, "rup": 3, "rmm": 4, "dyn": 7}
226
+
227
+
228
+ def _fcvt_round_up(rm: int, sign: int, guard: int, sticky: int, lsb: int) -> int:
229
+ """Whether the truncated magnitude rounds up by one ULP under the RISC-V
230
+ rounding mode (000 RNE, 001 RTZ, 010 RDN, 011 RUP, 100 RMM; 111 DYN has no
231
+ fcsr in this machine and follows RNE). guard is the half bit just below the
232
+ integer, sticky the OR of everything beneath it."""
233
+ if rm == 1: # RTZ: truncate
234
+ return 0
235
+ if rm == 2: # RDN: toward -inf
236
+ return 1 if (sign and (guard or sticky)) else 0
237
+ if rm == 3: # RUP: toward +inf
238
+ return 1 if ((not sign) and (guard or sticky)) else 0
239
+ if rm == 4: # RMM: nearest, ties away
240
+ return 1 if guard else 0
241
+ return 1 if (guard and (sticky or lsb)) else 0 # RNE / DYN
242
+
243
+
244
+ def fcvt_w_s_ref(w: int, rm: int = 1) -> int:
245
+ """float32 word -> signed int32 under rounding mode `rm`, saturating;
246
+ NaN -> INT_MAX, infinities -> INT_MIN/INT_MAX."""
247
  s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
248
  if e == 0xFF:
249
  return 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
250
+ mant = ((1 << 23) | f) if e else f # subnormal: no implicit 1
251
+ E = (e - 127) if e else -126 # subnormal exponent 1-bias
252
+ if E >= 31: # |value| >= 2^31: saturate
 
 
253
  if s and E == 31 and f == 0:
254
+ return 0x80000000 # exactly -2^31
255
  return 0x80000000 if s else 0x7FFFFFFF
256
+ if E >= 23:
257
+ mag = mant << (E - 23) # exact integer, no fraction
258
+ else:
259
+ sh = 23 - E
260
+ mag = mant >> sh
261
+ guard = (mant >> (sh - 1)) & 1
262
+ sticky = 1 if (mant & ((1 << (sh - 1)) - 1)) else 0
263
+ mag += _fcvt_round_up(rm, s, guard, sticky, mag & 1)
264
+ if s:
265
+ return 0x80000000 if mag >= 0x80000000 else (-mag) & MASK32
266
+ return 0x7FFFFFFF if mag > 0x7FFFFFFF else mag & MASK32
267
 
268
 
269
  def sext(v: int, bits: int) -> int:
 
369
  if mn == "fmv.x.w":
370
  return (0x70 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
371
  if mn == "fcvt.w.s":
372
+ rm = RM_NAMES.get(a[2], a[2]) if len(a) > 2 else 1 # default rtz
373
+ return (0x60 << 25) | (a[1] << 15) | (rm << 12) | (a[0] << 7) | 0x53
374
  if mn == "fcvt.s.w":
375
  return (0x68 << 25) | (a[1] << 15) | (a[0] << 7) | 0x53
376
  if mn in ("fsgnj.s", "fsgnjn.s", "fsgnjx.s"):
 
525
  from eval import float_bits_to_value
526
  x, y = float_bits_to_value(fa, 8, 23), float_bits_to_value(fb, 8, 23)
527
  wr(1 if [x <= y, x < y, x == y][f3] else 0)
528
+ elif f7 == 0x60: wr(fcvt_w_s_ref(FR[rs1], f3)) # FCVT.W.S (rm=funct3)
529
  elif f7 == 0x68: FR[rd] = fcvt_s_w_ref(R[rs1]) # FCVT.S.W
530
  elif f7 == 0x78: FR[rd] = a
531
  elif f7 == 0x70: wr(FR[rs1])
 
776
  exp += 1
777
  return (s << 31) | (exp << 23) | frac
778
 
779
+ def fcvt_w_s(self, w, rm=1):
780
+ """float32 -> signed int32 under rounding mode `rm`, saturating. The
781
+ shifts run through the barrel shifter; the round decision is control
782
+ logic, as in fcvt_s_w. NaN -> INT_MAX; overflow saturates."""
783
  s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
784
  if e == 0xFF:
785
+ return 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
786
+ mant = ((1 << 23) | f) if e else f
787
+ E = (e - 127) if e else -126
 
 
 
 
788
  if E >= 31:
789
  if s and E == 31 and f == 0:
790
  return 0x80000000 # exactly -2^31
 
792
  if E >= 23:
793
  mag = self.sll(mant, E - 23)
794
  else:
795
+ sh = 23 - E
796
+ mag = self.srl(mant, sh) if sh <= 31 else 0
797
+ guard = (mant >> (sh - 1)) & 1
798
+ sticky = 1 if (mant & ((1 << (sh - 1)) - 1)) else 0
799
+ mag += _fcvt_round_up(rm, s, guard, sticky, mag & 1)
800
+ if s:
801
+ return 0x80000000 if mag >= 0x80000000 else self.neg32(mag)
802
+ return 0x7FFFFFFF if mag > 0x7FFFFFFF else (mag & MASK32)
803
 
804
  def neur(self, x_reg, w_reg):
805
  x = int_to_bits(x_reg & 0xFF, 8) # MSB-first bits of low byte
 
1073
  s_out = self._bitwise("xor", sa << 31, sb << 31) >> 31 # FSGNJX.S
1074
  FR[rd] = (FR[rs1] & 0x7FFFFFFF) | (s_out << 31)
1075
  elif f7 == 0x60: # FCVT.W.S: float -> signed int, gate-routed
1076
+ wr(self.fcvt_w_s(FR[rs1], f3))
1077
  elif f7 == 0x68: # FCVT.S.W: signed int -> float, gate-routed
1078
  FR[rd] = self.fcvt_s_w(R[rs1])
1079
  elif f7 == 0x78:
 
1583
  return mem, 30, lambda s: s["regs"][3] == 8 and s["regs"][5] == (-5 & 0xFFFFFFFF)
1584
 
1585
 
1586
+ def prog_fcvt_rm():
1587
+ # FCVT.W.S honors the instruction's rounding-mode field. Convert 2.5, 3.5,
1588
+ # -2.5, 2.75 under several modes and check each against the IEEE result.
1589
+ import struct as _st
1590
+ a = Asm()
1591
+ a.lui(10, 0x1) # x10 = 0x1000 (data base)
1592
+ a.flw(1, 10, 0) # f1 = 2.5
1593
+ a.flw(2, 10, 4) # f2 = 3.5
1594
+ a.flw(3, 10, 8) # f3 = -2.5
1595
+ a.flw(4, 10, 12) # f4 = 2.75
1596
+ a.fcvt_w_s(1, 1, "rup") # x1 = ceil(2.5) = 3
1597
+ a.fcvt_w_s(2, 2, "rne") # x2 = 3.5 ties to even = 4
1598
+ a.fcvt_w_s(3, 1, "rne") # x3 = 2.5 ties to even = 2
1599
+ a.fcvt_w_s(4, 3, "rdn") # x4 = floor(-2.5) = -3
1600
+ a.fcvt_w_s(5, 4, "rtz") # x5 = trunc(2.75) = 2
1601
+ a.fcvt_w_s(6, 1, "rmm") # x6 = 2.5 ties away = 3
1602
+ a.ecall()
1603
+ mem = a.assemble()
1604
+ for i, v in enumerate((2.5, 3.5, -2.5, 2.75)):
1605
+ wb = _st.unpack("<I", _st.pack("<f", v))[0]
1606
+ for b in range(4):
1607
+ mem[0x1000 + i * 4 + b] = (wb >> (8 * b)) & 0xFF
1608
+ return mem, 40, lambda s: (s["regs"][1] == 3 and s["regs"][2] == 4
1609
+ and s["regs"][3] == 2 and s["regs"][4] == (-3 & 0xFFFFFFFF)
1610
+ and s["regs"][5] == 2 and s["regs"][6] == 3)
1611
+
1612
+
1613
  def rv32_netlist_check() -> bool:
1614
  """Verify the rv32-specific sub-circuits (NEUR, hazard, MULHU accumulator)
1615
  are recoverable and correct from the shipped .inputs metadata alone."""
 
1673
  return ok
1674
 
1675
 
1676
+ def fcvt_rm_check(cpu) -> bool:
1677
+ """FCVT.W.S under every rounding mode against an exact rational reference,
1678
+ for directed edge encodings (subnormals, ties, saturation) and randoms;
1679
+ the emulator reference and the gate-routed method must both match."""
1680
+ from fractions import Fraction
1681
+ import random
1682
+
1683
+ def value(w):
1684
+ s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
1685
+ if e == 0:
1686
+ v = Fraction(f, (1 << 23) * (1 << 126))
1687
+ else:
1688
+ v = Fraction((1 << 23) | f, 1 << 23) * (Fraction(2) ** (e - 127))
1689
+ return -v if s else v
1690
+
1691
+ def round_frac(v, rm):
1692
+ fl = v.numerator // v.denominator # floor (correct for negatives)
1693
+ frac = v - fl
1694
+ if frac == 0:
1695
+ return fl
1696
+ if rm == 1: # RTZ
1697
+ return fl + 1 if v < 0 else fl
1698
+ if rm == 2: # RDN
1699
+ return fl
1700
+ if rm == 3: # RUP
1701
+ return fl + 1
1702
+ if frac < Fraction(1, 2):
1703
+ return fl
1704
+ if frac > Fraction(1, 2):
1705
+ return fl + 1
1706
+ if rm == 4: # RMM: ties away from zero
1707
+ return fl + 1 if v > 0 else fl
1708
+ return fl if fl % 2 == 0 else fl + 1 # RNE: ties to even
1709
+
1710
+ def sat(iv):
1711
+ if iv > 0x7FFFFFFF:
1712
+ return 0x7FFFFFFF
1713
+ if iv < -0x80000000:
1714
+ return 0x80000000
1715
+ return iv & MASK32
1716
+
1717
+ words = []
1718
+ for s in (0, 1):
1719
+ for e in (0, 1, 100, 125, 126, 127, 128, 130, 149, 150, 157, 158, 254, 255):
1720
+ for f in (0, 1, 0x400000, 0x7FFFFF):
1721
+ words.append((s << 31) | (e << 23) | f)
1722
+ rng = random.Random(0xFCF7)
1723
+ words += [rng.getrandbits(32) for _ in range(600)]
1724
+
1725
+ bad = None
1726
+ for w in words:
1727
+ s, e, f = (w >> 31) & 1, (w >> 23) & 0xFF, w & 0x7FFFFF
1728
+ for rm in (0, 1, 2, 3, 4):
1729
+ if e == 0xFF:
1730
+ exp = 0x7FFFFFFF if f else (0x80000000 if s else 0x7FFFFFFF)
1731
+ else:
1732
+ exp = sat(round_frac(value(w), rm))
1733
+ ref = fcvt_w_s_ref(w, rm)
1734
+ gate = cpu.fcvt_w_s(w, rm)
1735
+ if not (ref == gate == exp):
1736
+ bad = (w, rm, ref, gate, exp)
1737
+ break
1738
+ if bad:
1739
+ break
1740
+ if bad:
1741
+ w, rm, ref, gate, exp = bad
1742
+ print(f" fcvt rounding-mode check FAIL w={w:08x} rm={rm} "
1743
+ f"ref={ref:08x} gate={gate:08x} exp={exp:08x}")
1744
+ return False
1745
+ print(f" fcvt rounding-mode check ({len(words)} words x 5 modes vs exact) PASS")
1746
+ return True
1747
+
1748
+
1749
  def rv32_test() -> int:
1750
  print("Loading neural_rv32...")
1751
  cpu = Rv32ThresholdCPU()
 
1757
  ("muldiv", prog_muldiv),
1758
  ("float_subset", prog_float),
1759
  ("fcvt_roundtrip", prog_fcvt),
1760
+ ("fcvt_rounding_modes", prog_fcvt_rm),
1761
  ("neur_xor_net", prog_neur),
1762
  ("mmio_print", prog_mmio),
1763
  ]
 
1766
  mem, budget, check = builder()
1767
  all_ok &= lockstep(cpu, mem, budget, name, check)
1768
  all_ok &= rv32_netlist_check()
1769
+ all_ok &= fcvt_rm_check(cpu)
1770
  print(f"dual-issue pairs retired: {cpu.pairs_issued}")
1771
  print("RV32:", "PASS" if all_ok else "FAIL")
1772
  return 0 if all_ok else 1
todo.md CHANGED
@@ -4,7 +4,6 @@ Unfinished work.
4
 
5
  - The float add, multiply, and divide pipelines handle subnormal operands and produce subnormal results rather than flushing them to zero.
6
  - The F extension includes a fused multiply-add instruction that rounds once.
7
- - FCVT.W.S honors the instruction's rounding-mode field instead of always rounding toward zero.
8
  - Program-counter sequencing and instruction decode are computed by threshold gates rather than by fixed wiring in the runtimes.
9
  - The standalone float normalize stages, superseded by the composed pipelines' own normalizers, are removed from the circuit inventory.
10
  - Every gate in the 32-bit multiply circuit resolves its .inputs to a defined signal.
 
4
 
5
  - The float add, multiply, and divide pipelines handle subnormal operands and produce subnormal results rather than flushing them to zero.
6
  - The F extension includes a fused multiply-add instruction that rounds once.
 
7
  - Program-counter sequencing and instruction decode are computed by threshold gates rather than by fixed wiring in the runtimes.
8
  - The standalone float normalize stages, superseded by the composed pipelines' own normalizers, are removed from the circuit inventory.
9
  - Every gate in the 32-bit multiply circuit resolves its .inputs to a defined signal.