tricu

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

float_significands.py (9755B)


      1 #!/usr/bin/env python3
      2 """Exact-integer-checked significand kernels; isolated, opt-in benchmark."""
      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 
     15 ROOT = Path(__file__).resolve().parent.parent
     16 
     17 
     18 def sha(path):
     19     return hashlib.sha256(path.read_bytes()).hexdigest()
     20 
     21 
     22 def msb(n):
     23     return tree_list([TRUE if b == '1' else LEAF for b in bin(n)[2:]]) if n else LEAF
     24 
     25 
     26 def rounded(n, k):
     27     # Counts above the width return zero and the input nonzero predicate directly.
     28     if k > n.bit_length():
     29         return 0, bool(n)
     30     q, rem = divmod(n, 1 << k)
     31     twice = rem * 2
     32     return q + (twice > (1 << k) or (twice == (1 << k) and bool(q & 1))), bool(rem)
     33 
     34 
     35 def shifted(n, k):
     36     q = n >> k if k <= n.bit_length() else 0
     37     return q, n != (q << k) if q else bool(n)
     38 
     39 
     40 def chained(n, k):
     41     for _ in range(3):
     42         n = rounded(n, k)[0]
     43     return n
     44 
     45 
     46 def validation_cases():
     47     rows = [(n,k) for n in range(64) for k in range(10)]
     48     for w in [1, 2, 8, 24, 53, 54, 106]:
     49         for n in [1 << (w-1), (1 << w)-1, (1 << (w-1))+1]:
     50             rows += [(n,k) for k in [0,1,2,w-1,w,w+1,2046,1 << 128]]
     51     # Explicit below/tie/above cases, even and odd retained quotients, carry growth.
     52     for q in [0,1,2,3,6,7,(1 << 52)-1,(1 << 53)-1]:
     53         for k in [1,2,7,53]:
     54             rows += [((q << k)+(1 << (k-1))+delta,k) for delta in [-1,0,1]]
     55     return list(dict.fromkeys(rows))
     56 
     57 
     58 def timed_cases(size, width, seed):
     59     rng = random.Random(seed)
     60     counts = [0,1,2,3,7,8,13,width-1,width,width+1,2046,1 << 128]
     61     return [(rng.getrandbits(width) | (1 << (width-1)), counts[i % len(counts)])
     62             for i in range(size)]
     63 
     64 
     65 # (export, input representation, result kind)
     66 JOBS = [('lsbShift','lsb','shift'), ('lsbRound','lsb','round'),
     67         ('lsbCachedShift','cachedL','shift'), ('lsbCachedRound','cachedL','round'),
     68         ('lsbBuildCachedRound','lsb','round'),
     69         ('msbShift','msb','shift'), ('msbRound','msb','round'),
     70         ('msbCachedShift','cached','shift'), ('msbCachedRound','cached','round'),
     71         ('msbBuildCachedShift','msb','shift'), ('msbBuildCachedRound','msb','round'),
     72         ('msbMaintainedRound','cached','maintained'),
     73         ('lsbChain','lsb','chain'), ('msbChain','msb','chain'),
     74         ('msbCachedChain','cached','chain'), ('msbMaintainedChain','cached','chain'),
     75         ('lsbNormalize','lsb','normalize'), ('msbNormalize','msb','normalize')]
     76 
     77 
     78 def encode_input(rows, layout):
     79     def value(n):
     80         if layout == 'lsb':
     81             return nat(n)
     82         bits = nat(n) if layout == 'cachedL' else msb(n)
     83         return pair(nat(n.bit_length()), bits) if layout in ['cached','cachedL'] else bits
     84     return tree_list([pair(nat(k), value(n)) for n,k in rows])
     85 
     86 
     87 def expected(rows, layout, kind):
     88     encode = nat if layout in ['lsb','cachedL'] else msb
     89     values = []
     90     for n,k in rows:
     91         if kind == 'normalize':
     92             zeros = (n & -n).bit_length()-1 if n else 0
     93             values.append(pair(nat(zeros),encode(n >> zeros)))
     94             continue
     95         if kind == 'chain':
     96             values.append(encode(chained(n,k)))
     97             continue
     98         q, sticky = shifted(n,k) if kind == 'shift' else rounded(n,k)
     99         value = encode(q)
    100         if kind == 'maintained':
    101             value = pair(nat(q.bit_length()), value)
    102         values.append(pair(value, TRUE if sticky else LEAF))
    103     return tree_list(values)
    104 
    105 
    106 def main():
    107     p = argparse.ArgumentParser(description=__doc__)
    108     p.add_argument('tricu', type=Path)
    109     p.add_argument('--directory', required=True, type=Path)
    110     p.add_argument('--size', type=int, default=96)
    111     p.add_argument('--repeats', type=int, default=5)
    112     p.add_argument('--seconds', type=int, default=900)
    113     args = p.parse_args()
    114     if not 12 <= args.size <= 1024 or not 3 <= args.repeats <= 10:
    115         p.error('size must be 12..1024; repeats 3..10')
    116     root = args.directory.resolve()
    117     root.mkdir(parents=True, exist_ok=False)
    118     workspace = root/'workspace'
    119     workspace.mkdir()
    120     sources = {'driver.tri': ROOT/'bench/float-significands.tri',
    121                'native_math.tri': ROOT/'lib/math/native.tri'}
    122     for name,path in sources.items():
    123         (workspace/name).write_bytes(path.read_bytes())
    124     runner = ProbeRunner(args.tricu, root, args.seconds, memory_mib=2048)
    125     report = dict(complete=False, scope='fresh-process-invoke-through-full-force',
    126                   runtime_sha256=sha(args.tricu), harness_sha256=sha(Path(__file__)),
    127                   compiler_sha256=sha(ROOT/'bootstrap/build-stable.arboricx'),
    128                   platform=platform.platform(), sources={k:sha(v) for k,v in sources.items()},
    129                   samples=[], summaries=[], validation=[])
    130     try:
    131         tool = runner.imported(ROOT/'bootstrap/build-stable.arboricx', 'compiler')
    132         bundle = root/'significands.arboricx'
    133         runner.run('compile', ['compile','--file',workspace/'driver.tri',
    134             '--toolchain',tool,'--tool-store',runner.store,'--store',runner.store,
    135             '--output',bundle,'--compile-timeout','240'], 270)
    136         pin = runner.imported(bundle, 'significands')
    137         report['bundle_sha256'] = sha(bundle)
    138 
    139         def run(label, name, raw, wanted, measure=False):
    140             path = root/(label+'.input.tree')
    141             path.write_bytes(raw)
    142             argv = ['--arg-tree-file',path] + (['--profile-full-result'] if measure else [])
    143             actual = runner.invoke(label,pin,name,argv)
    144             expect(actual,wanted,label)
    145             if not measure:
    146                 report['validation'].append(dict(label=label,input_sha256=hashlib.sha256(raw).hexdigest(),
    147                                                 output_sha256=hashlib.sha256(actual).hexdigest()))
    148                 return
    149             log = (root/(label+'.log')).read_text()
    150             samples = {v['profile']:v for line in log.splitlines() if line.startswith('{')
    151                        for v in [json.loads(line)]}
    152             before, after = samples['invoke-begin-1'], samples['force-end-1']
    153             return dict(wall_ns=after['wall_ns']-before['wall_ns'],
    154                         allocated_bytes=after['allocated_bytes']-before['allocated_bytes'])
    155 
    156         rows = validation_cases()
    157         report['validation_case_count'] = len(rows)
    158         for name,layout,kind in JOBS:
    159             run('check-'+name,name,encode_input(rows,layout),expected(rows,layout,kind))
    160         for name in ['zeroShiftDemand','zeroRoundDemand','zeroMsbDemand']:
    161             run(name,name,LEAF,LEAF)
    162         for name in ['hugeShift','hugeRound']:
    163             run(name,name,nat(1 << 128),pair(LEAF,TRUE))
    164 
    165         schedule = []
    166         for width in [53,106]:
    167             rows = timed_cases(args.size,width,640000+width)
    168             for name,layout,kind in JOBS:
    169                 schedule.append((width,'mixed',name,encode_input(rows,layout),expected(rows,layout,kind)))
    170             numbers = [n for n,k in rows]
    171             schedule += [(width,'mixed','lsbToMsb',tree_list([nat(n) for n in numbers]),tree_list([msb(n) for n in numbers])),
    172                          (width,'mixed','msbToLsb',tree_list([msb(n) for n in numbers]),tree_list([nat(n) for n in numbers])),
    173                          (width,'mixed','widths',tree_list([nat(n) for n in numbers]),nat([width]*len(numbers)))]
    174             selected = {'lsbRound','lsbCachedRound','msbRound','msbCachedRound','msbMaintainedRound'}
    175             for group,counts in [('small',[1,2,3,7]),('boundary',[width-1,width,width+1]),('far',[2046,1 << 128])]:
    176                 grouped = [(n,counts[i % len(counts)]) for i,(n,_) in enumerate(rows)]
    177                 for name,layout,kind in JOBS:
    178                     if name in selected:
    179                         schedule.append((width,group,name,encode_input(grouped,layout),expected(grouped,layout,kind)))
    180             for group in ['smallCarry','boundarySparse']:
    181                 counts = [1,2,3,7] if group == 'smallCarry' else [width-1,width,width+1]
    182                 special = [((1 << width)-1 if group == 'smallCarry' else (1 << (width-1))+(i % 2),
    183                             counts[i % len(counts)]) for i in range(args.size)]
    184                 for name,layout,kind in JOBS:
    185                     if name in selected or kind == 'normalize':
    186                         schedule.append((width,group,name,encode_input(special,layout),expected(special,layout,kind)))
    187         rng = random.Random(2653)
    188         for repeat in range(args.repeats):
    189             rng.shuffle(schedule)
    190             for width,group,name,raw,wanted in schedule:
    191                 sample = run(f'{name}-{width}-{group}-{repeat}',name,raw,wanted,True)
    192                 report['samples'].append(dict(name=name,width=width,group=group,size=args.size,repeat=repeat,
    193                                               warmup=repeat==0,**sample))
    194         for width,group,name,_,_ in sorted(schedule):
    195             samples = [r for r in report['samples'] if r['name']==name and r['width']==width and r['group']==group and not r['warmup']]
    196             row = dict(name=name,width=width,group=group,size=args.size)
    197             for key in ['wall_ns','allocated_bytes']:
    198                 row[key] = statistics.median(r[key] for r in samples)
    199                 row[key+'_min'] = min(r[key] for r in samples)
    200                 row[key+'_max'] = max(r[key] for r in samples)
    201             report['summaries'].append(row)
    202             print(json.dumps(row),flush=True)
    203         report['complete'] = True
    204     finally:
    205         (root/'report.json').write_text(json.dumps(report,indent=2)+'\n')
    206 
    207 
    208 if __name__ == '__main__':
    209     main()