float_quantize.py (6709B)
1 #!/usr/bin/env python3 2 """Positive RNE quantizer checked against exact distances between IEEE values.""" 3 import argparse 4 from fractions import Fraction 5 import hashlib 6 import json 7 from pathlib import Path 8 import random 9 10 from float_multiply import Runner, sha 11 from base_workloads import encode as nat 12 from http_headers import LEAF, TRUE, pair, tree_list, expect 13 14 ROOT = Path(__file__).resolve().parent.parent 15 BIAS = 2304 16 INF = 0x7ff0000000000000 17 MAX = INF-1 18 19 20 def dyadic(n, e): 21 return Fraction(n << e) if e >= 0 else Fraction(n, 1 << -e) 22 23 24 def value(bits): 25 exponent, fraction = divmod(bits, 1 << 52) 26 assert 0 <= bits < INF 27 return dyadic(fraction, -1074) if exponent == 0 else dyadic((1 << 52)+fraction, exponent-1075) 28 29 30 def oracle(n, scale): 31 """Binary-search adjacent IEEE encodings, then compare exact distances. 32 33 The oracle searches the IEEE encoding space and compares exact rational 34 distances. Infinity uses the virtual next value 2^1024. 35 """ 36 assert n >= 0 and scale >= 0 37 x = dyadic(n, scale-BIAS) 38 if x >= value(MAX): 39 bits = INF if x >= (value(MAX)+dyadic(1,1024))/2 else MAX 40 else: 41 lo,hi = 0,MAX 42 while lo+1 < hi: 43 mid = (lo+hi)//2 44 if value(mid) <= x: 45 lo = mid 46 else: 47 hi = mid 48 below,above = x-value(lo),value(hi)-x 49 bits = lo if below < above or (below == above and lo % 2 == 0) else hi 50 inexact = bits == INF or value(bits) != x 51 return bits,inexact,inexact and bits < (1 << 52) 52 53 54 def encoded(result): 55 bits,inexact,underflow = result 56 return pair(nat(bits),pair(TRUE if inexact else LEAF,TRUE if underflow else LEAF)) 57 58 59 def cases(): 60 rows = [(0,0),(0,4608)] 61 # Halfway, just below and just above zero/subnormal/normal transitions. 62 for m in [0,1,2,3,2**51-1,2**51,2**52-2,2**52-1,2**52,2**53-2]: 63 for delta in [-1,0,1]: 64 rows.append(((2*m+1)*4+delta,BIAS-1077)) 65 rows.append((m,BIAS-1074)) 66 # Precision carries and both sides of binade boundaries, including overflow. 67 for e in [-1074,-1073,-53,-52,0,970,971]: 68 for m in [2**52,2**52+1,2**53-2,2**53-1]: 69 for delta in [-1,0,1]: 70 rows.append((m*8+4+delta,BIAS+e-3)) 71 # Short exact cancellation magnitudes exercise left normalization. 72 rows += [(n,BIAS+e) for n in [1,2,3,7,2**25-1,2**52-1] 73 for e in [-1126,-1074,-1073,-52,0,971,1023]] 74 # Explicit double-rounding counterexamples at the subnormal precision boundary. 75 rows += [(2**54+5,BIAS-1077),(2**54+11,BIAS-1077)] 76 rng = random.Random(23041074) 77 rows += [(rng.getrandbits(rng.randrange(1,217)),rng.randrange(900,3350)) for _ in range(256)] 78 rows += [(1,0),(2**216-1,0),(1,4608),(2**216-1,4608)] 79 return list(dict.fromkeys(rows)) 80 81 82 def near_cases(): 83 lo,hi = 2**52,2**53-1 84 pairs = [(lo,lo),(lo+1,lo),(lo+3,lo),(hi,hi-1),(hi,lo),(hi,hi)] 85 return [(scale,gap,a,b) for scale in [1178,1228,1230,1231,2252,3221,3275] 86 for gap in [0,1,27,53,54] for a,b in pairs] 87 88 89 def main(): 90 p = argparse.ArgumentParser(description=__doc__) 91 p.add_argument('tricu',type=Path) 92 p.add_argument('--directory',required=True,type=Path) 93 args = p.parse_args() 94 root=args.directory.resolve(); root.mkdir(parents=True,exist_ok=False) 95 work=root/'workspace'; work.mkdir() 96 sources={'driver.tri':'bench/float-quantize.tri','pipelines.tri':'bench/float-pipelines.tri', 97 'kernels.tri':'bench/float-significands.tri','layouts.tri':'bench/float-layouts.tri', 98 'native_math.tri':'lib/math/native.tri'} 99 for name,path in sources.items(): (work/name).write_bytes((ROOT/path).read_bytes()) 100 report=dict(complete=False,scope='positive RNE; tininess after rounding; experimental wire encoding', 101 runtime_sha256=sha(args.tricu),compiler_sha256=sha(ROOT/'bootstrap/build-stable.arboricx'), 102 harness_sha256=sha(Path(__file__)),sources={n:sha(ROOT/p) for n,p in sources.items()},validation=[],demand=[]) 103 runner=Runner(args.tricu,root,1200,memory_mib=2048) 104 try: 105 tool=runner.imported(ROOT/'bootstrap/build-stable.arboricx','compiler') 106 bundle=root/'quantize.arboricx' 107 runner.run('compile',['compile','--file',work/'driver.tri','--toolchain',tool, 108 '--tool-store',runner.store,'--store',runner.store,'--output',bundle,'--compile-timeout','240'],270) 109 pin=runner.imported(bundle,'quantize'); report['bundle_sha256']=sha(bundle) 110 def check(label,name,raw,wanted,count): 111 path=root/(label+'.input.tree');path.write_bytes(raw) 112 actual=runner.invoke(label,pin,name,['--arg-tree-file',path]) 113 expect(actual,wanted,label) 114 report['validation'].append(dict(label=label,cases=count,input_sha256=sha(path), 115 output_sha256=hashlib.sha256(actual).hexdigest())) 116 rows=cases();report['quantizer_cases']=len(rows) 117 for offset in range(0,len(rows),24): 118 batch=rows[offset:offset+24] 119 check(f'quantize-{offset}','quantized',tree_list([pair(nat(s),nat(n)) for n,s in batch]), 120 tree_list([encoded(oracle(n,s)) for n,s in batch]),len(batch)) 121 near=near_cases();report['near_cases_per_operation']=len(near) 122 for name,sign in [('nearAdds',1),('nearSubtracts',-1)]: 123 for offset in range(0,len(near),24): 124 batch=near[offset:offset+24] 125 raw=tree_list([pair(nat(s),pair(nat(g),pair(nat(a),nat(b)))) for s,g,a,b in batch]) 126 wanted=tree_list([encoded(oracle((a << g)+sign*b,s)) for s,g,a,b in batch]) 127 check(f'{name}-{offset}',name,raw,wanted,len(batch)) 128 controls=[(2**54+5,2**51),(2**54+11,2**51+2)] 129 raw=tree_list([pair(nat(BIAS-1077),nat(n)) for n,_ in controls]) 130 wanted=tree_list([encoded((wrong,True,True)) for _,wrong in controls]) 131 check('double-round-negative-control','doubleRoundControl',raw,wanted,len(controls)) 132 assert all(oracle(n,BIAS-1077)[0] != wrong for n,wrong in controls) 133 report['double_rounding_counterexamples']=len(controls) 134 actual=runner.invoke('demand-zero',pin,'zeroDemand',['--arg','0']) 135 expect(actual,encoded((0,False,False)),'zero does not demand scale') 136 report['demand'].append(dict(name='zeroDemand',outcome='expected-result')) 137 report['complete']=True 138 finally: 139 (root/'report.json').write_text(json.dumps(report,indent=2)+'\n') 140 print('PASS',report['quantizer_cases'],'quantizer cases;',report['near_cases_per_operation'], 141 'near-alignment cases per operation; zero demand control') 142 143 144 if __name__=='__main__': main()