tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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()