tricu

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

float_pipelines.py (10679B)


      1 #!/usr/bin/env python3
      2 """Blocked significands and bounded exact arithmetic followed by 53-bit RNE."""
      3 import argparse
      4 import hashlib
      5 import json
      6 import platform
      7 import random
      8 import statistics
      9 from pathlib import Path
     10 
     11 from collections_batch import ProbeRunner
     12 from base_workloads import encode as nat
     13 from http_headers import LEAF, TRUE, pair, tree_list, expect
     14 from float_significands import rounded, shifted, validation_cases
     15 
     16 ROOT = Path(__file__).resolve().parent.parent
     17 
     18 
     19 def sha(path):
     20     return hashlib.sha256(path.read_bytes()).hexdigest()
     21 
     22 
     23 def limbs(n):
     24     out = []
     25     while n:
     26         out.append(nat(n & 255))
     27         n >>= 8
     28     return tree_list(out)
     29 
     30 
     31 def finish(n):
     32     cut = max(0,n.bit_length()-53)
     33     m,flag = rounded(n,cut)
     34     if m.bit_length() > 53:
     35         m >>= 1
     36         cut += 1
     37     return cut,m,flag
     38 
     39 
     40 def operation(name,a,b,gap=0):
     41     if name == 'Chain':
     42         total,flag = 0,False
     43         for _ in range(3):
     44             cut,a,f = finish(a*b)
     45             total += cut
     46             flag |= f
     47         return total,a,flag
     48     if name in ['Add','AlignedAdd']:
     49         n = (a << gap)+b
     50     elif name in ['Subtract','AlignedSubtract']:
     51         n = (a << gap)-b
     52         assert n >= 0
     53     else:
     54         n = a*b
     55     return finish(n)
     56 
     57 
     58 def encoded_result(result, encode):
     59     cut,m,flag = result
     60     return pair(nat(cut),pair(encode(m),TRUE if flag else LEAF))
     61 
     62 
     63 def job(rows,layout,op):
     64     encode = nat if layout == 'lsb' else limbs
     65     raw = []
     66     wanted = []
     67     for a,b,gap in rows:
     68         argument = pair(encode(a),encode(b))
     69         if op.startswith('Aligned'):
     70             argument = pair(nat(gap),argument)
     71         raw.append(argument)
     72         wanted.append(encoded_result(operation(op,a,b,gap),encode))
     73     return tree_list(raw),tree_list(wanted)
     74 
     75 
     76 def main():
     77     p = argparse.ArgumentParser(description=__doc__)
     78     p.add_argument('tricu',type=Path)
     79     p.add_argument('--directory',required=True,type=Path)
     80     p.add_argument('--size',type=int,default=16)
     81     p.add_argument('--repeats',type=int,default=5)
     82     p.add_argument('--seconds',type=int,default=1200)
     83     args = p.parse_args()
     84     if not 4 <= args.size <= 64 or not 3 <= args.repeats <= 10:
     85         p.error('size 4..64; repeats 3..10')
     86     root = args.directory.resolve()
     87     root.mkdir(parents=True,exist_ok=False)
     88     workspace = root/'workspace'
     89     workspace.mkdir()
     90     sources = {'driver.tri':ROOT/'bench/float-pipelines.tri',
     91                'kernels.tri':ROOT/'bench/float-significands.tri',
     92                'layouts.tri':ROOT/'bench/float-layouts.tri',
     93                'native_math.tri':ROOT/'lib/math/native.tri'}
     94     for name,path in sources.items():
     95         (workspace/name).write_bytes(path.read_bytes())
     96     runner = ProbeRunner(args.tricu,root,args.seconds,memory_mib=2048)
     97     report = dict(complete=False,scope='fresh-process-invoke-through-full-force',
     98                   runtime_sha256=sha(args.tricu),harness_sha256=sha(Path(__file__)),
     99                   oracle_dependency_sha256=sha(ROOT/'test/float_significands.py'),
    100                   compiler_sha256=sha(ROOT/'bootstrap/build-stable.arboricx'),platform=platform.platform(),
    101                   sources={name:sha(path) for name,path in sources.items()},validation=[],samples=[],summaries=[])
    102     try:
    103         tool = runner.imported(ROOT/'bootstrap/build-stable.arboricx','compiler')
    104         bundle = root/'pipelines.arboricx'
    105         runner.run('compile',['compile','--file',workspace/'driver.tri','--toolchain',tool,
    106             '--tool-store',runner.store,'--store',runner.store,'--output',bundle,'--compile-timeout','240'],270)
    107         pin = runner.imported(bundle,'pipelines')
    108         report['bundle_sha256'] = sha(bundle)
    109 
    110         def run(label,name,raw,wanted,measure=False):
    111             path = root/(label+'.input.tree')
    112             path.write_bytes(raw)
    113             actual = runner.invoke(label,pin,name,['--arg-tree-file',path]+
    114                                    (['--profile-full-result'] if measure else []))
    115             expect(actual,wanted,label)
    116             if not measure:
    117                 report['validation'].append(dict(label=label,input_sha256=hashlib.sha256(raw).hexdigest(),
    118                                                 output_sha256=hashlib.sha256(actual).hexdigest()))
    119                 return
    120             log = (root/(label+'.log')).read_text()
    121             points = {v['profile']:v for line in log.splitlines() if line.startswith('{')
    122                       for v in [json.loads(line)]}
    123             before,after = points['invoke-begin-1'],points['force-end-1']
    124             return dict(wall_ns=after['wall_ns']-before['wall_ns'],
    125                         allocated_bytes=after['allocated_bytes']-before['allocated_bytes'])
    126 
    127         cases = validation_cases()
    128         report['shift_round_cases'] = len(cases)
    129         for offset in range(0,len(cases),96):
    130             rows = cases[offset:offset+96]
    131             raw = tree_list([pair(nat(k),limbs(n)) for n,k in rows])
    132             for op,oracle in [('Shift',shifted),('Round',rounded)]:
    133                 wanted = tree_list([pair(limbs(q),TRUE if f else LEAF)
    134                                     for q,f in [oracle(n,k) for n,k in rows]])
    135                 run(f'check-limb{op}-{offset}','limb'+op,raw,wanted)
    136         rng = random.Random(25653)
    137         special = [0,1,2,127,128,254,255,256,257,65535,65536,(1 << 52)-1,
    138                    1 << 52,(1 << 53)-1,(1 << 53)-2]
    139         pairs = [(max(a,b),min(a,b),0) for a in special for b in special]
    140         pairs += [(max(a,b),min(a,b),0) for a,b in
    141                   [(rng.getrandbits(53),rng.getrandbits(53)) for _ in range(32)]]
    142         report['arithmetic_cases'] = len(pairs)
    143         for op in ['Add','Subtract','Multiply','Chain']:
    144             for layout in ['lsb','limb']:
    145                 for offset in range(0,len(pairs),24):
    146                     raw,wanted = job(pairs[offset:offset+24],layout,op)
    147                     run(f'check-{layout}{op}-{offset}',layout+op,raw,wanted)
    148         aligned = [(a,b,k) for a,b,_ in pairs[::8] for k in [0,1,7,8,52,53,54]]
    149         aligned += [((1 << 53)-1,1,1),((1 << 53)-1,0,1),((1 << 53)-1,2,1)]
    150         report['alignment_cases'] = len(aligned)
    151         for op in ['AlignedAdd','AlignedSubtract']:
    152             for layout in ['lsb','limb']:
    153                 for offset in range(0,len(aligned),24):
    154                     raw,wanted = job(aligned[offset:offset+24],layout,op)
    155                     run(f'check-{layout}{op}-{offset}',layout+op,raw,wanted)
    156         # Exercise the final carry-renormalization path directly, including
    157         # below/tie/above cases near the next power of two.
    158         boundaries = [0,1] + [n for w in [54,55,105,106]
    159                               for n in [(1 << w)-1,(1 << w)-(1 << (w-54)),
    160                                         (1 << w)-(1 << (w-54))-1,1 << w]]
    161         for i,n in enumerate(boundaries):
    162             for name,encode in [('finishL',nat),('finishB',limbs)]:
    163                 run(f'check-finish-{name}-{i}',name,encode(n),encoded_result(finish(n),encode))
    164         report['finish_boundary_cases'] = len(boundaries)
    165         values = special+[rng.getrandbits(106) for _ in range(16)]
    166         run('check-convert-in','naturalToLimb',nat(values),tree_list([limbs(v) for v in values]))
    167         run('check-convert-out','limbToNatural',tree_list([limbs(v) for v in values]),nat(values))
    168         run('check-widths','limbWidths',tree_list([limbs(v) for v in values]),nat([v.bit_length() for v in values]))
    169 
    170         groups = {}
    171         groups['random'] = [(a,b,0) for a,b in [sorted([rng.getrandbits(52)+(1 << 52),
    172                                                        rng.getrandbits(52)+(1 << 52)],reverse=True)
    173                                                 for _ in range(args.size)]]
    174         groups['carry'] = [((1 << 53)-1,(1 << 53)-1-(i%3),0) for i in range(args.size)]
    175         groups['cancel'] = [(a,a-(i%4),0) for i,(a,_,_) in enumerate(groups['random'])]
    176         schedule = []
    177         for group,rows in groups.items():
    178             for op in ['Add','Subtract','Multiply','Chain']:
    179                 for layout in ['lsb','limb']:
    180                     raw,wanted = job(rows,layout,op)
    181                     schedule.append((group,layout+op,raw,wanted))
    182         for group,gaps in [('near',[0,1,2,7]),('wide',[8,52,53,54])]:
    183             rows = [(a,b,gaps[i%len(gaps)]) for i,(a,b,_) in enumerate(groups['random'])]
    184             for op in ['AlignedAdd','AlignedSubtract']:
    185                 for layout in ['lsb','limb']:
    186                     raw,wanted = job(rows,layout,op)
    187                     schedule.append((group,layout+op,raw,wanted))
    188         for width in [53,106]:
    189             values = [rng.getrandbits(width-1)+(1 << (width-1)) for _ in range(args.size)]
    190             for group,counts in [('small',[1,2,3,7]),('byte',[8,16,24,32]),
    191                                  ('boundary',[width-1,width,width+1]),('far',[2046,1 << 128])]:
    192                 rows = [(n,counts[i%len(counts)]) for i,n in enumerate(values)]
    193                 for layout,encode in [('lsb',nat),('limb',limbs)]:
    194                     raw = tree_list([pair(nat(k),encode(n)) for n,k in rows])
    195                     wanted = tree_list([pair(encode(q),TRUE if f else LEAF) for q,f in [rounded(n,k) for n,k in rows]])
    196                     name = layout+'Round'
    197                     schedule.append((f'{width}-{group}',name,raw,wanted))
    198             schedule += [(str(width),'naturalToLimb',nat(values),tree_list([limbs(v) for v in values])),
    199                          (str(width),'limbToNatural',tree_list([limbs(v) for v in values]),nat(values))]
    200         rng.shuffle(schedule)
    201         for repeat in range(args.repeats):
    202             rng.shuffle(schedule)
    203             for group,name,raw,wanted in schedule:
    204                 sample = run(f'{group}-{name}-{repeat}',name,raw,wanted,True)
    205                 report['samples'].append(dict(group=group,name=name,size=args.size,repeat=repeat,
    206                                               warmup=repeat==0,**sample))
    207         for group,name,_,_ in sorted(schedule):
    208             samples = [v for v in report['samples'] if v['group']==group and v['name']==name and not v['warmup']]
    209             row = dict(group=group,name=name,size=args.size)
    210             for key in ['wall_ns','allocated_bytes']:
    211                 row[key] = statistics.median(v[key] for v in samples)
    212                 row[key+'_min'] = min(v[key] for v in samples)
    213                 row[key+'_max'] = max(v[key] for v in samples)
    214             report['summaries'].append(row)
    215             print(json.dumps(row),flush=True)
    216         report['complete'] = True
    217     finally:
    218         (root/'report.json').write_text(json.dumps(report,indent=2)+'\n')
    219 
    220 
    221 if __name__ == '__main__':
    222     main()