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