import argparse import copy import hashlib import json from pathlib import Path import re import struct import subprocess import tempfile def earliest_events(report): graph = report['timing_graph'] incoming = [0] * len(graph['events']) outgoing = [[] for _ in incoming] for edge in graph['dependencies']: if edge['distance'] == 0: incoming[edge['target']] += 1 outgoing[edge['source']].append(edge) ready = [i for i, count in enumerate(incoming) if count == 0] times = [0] * len(incoming) for source in ready: for edge in outgoing[source]: target = edge['target'] times[target] = max(times[target], times[source] + edge['delay_ns']) incoming[target] -= 1 if incoming[target] == 0: ready.append(target) assert len(ready) == len(times), 'within-generation graph is cyclic' return graph, times def record(op, arg=0, a=0, b=0, length=0): return struct.pack('= 280 assert queued['completed_generations'] >= 4 assert any(core['queue_wait_ns'] > 0 for core in queued['per_core']) sizes = run([[scalar, load(1), scalar], [scalar, load(65), scalar]], controller=True, issue=True) assert sizes['global_memory_service_ns'] == 160 assert sizes['memory_controller']['interval_ns'] >= 160 delayed = [barrier(1) + [load(8192)], barrier(0) + [load(64)]] behind = run(delayed, controller=True, issue=True) assert behind['memory_controller']['status'] == 'estimated' reader = next(core for core in behind['memory_controller']['per_core'] if core['core'] == 1) assert reader['queue_wait_ns'] > 0 zero = run([barrier(1) + [record(27, length=128), load(0)], barrier(0) + [store(128)]], controller=True, issue=True) reader = next(core for core in zero['memory_controller']['per_core'] if core['core'] == 0) assert reader['queue_wait_ns'] > 0 # Zero service still queues behind an active request. bounded = json.loads(run([[scalar, scalar], [scalar, load(65536), scalar]], controller=True, issue=True, valid=False).stdout) assert bounded['status'] == 'unavailable' assert bounded['memory_controller']['status'] == 'unavailable' assert 'periodic_state' not in bounded['memory_controller'] shared = [[record(31, a=0, b=2), store(128), record(32, arg=1, a=1), record(32, arg=2, a=1)], [record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(29, arg=2, length=64)], [record(32, arg=0, a=0), record(31, a=1, b=1), load(128), record(30, arg=1, length=64)]] publication = run(shared, controller=True, issue=True) assert publication['memory_controller']['status'] == 'estimated' assert publication['global_write_requests'] == 1 and publication['global_read_requests'] == 2 assert publication['direct_requests'] == 1 return [staggered, burst, [[scalar, load(33), scalar], [scalar, store(128), scalar]], [[scalar, load(1), scalar], [scalar, load(65), scalar]], delayed, shared] def main(): parser = argparse.ArgumentParser() parser.add_argument('--diagnostic', required=True) parser.add_argument('--pimsim', help='Optional ChipTest executable for matched fast-mode timing checks') args = parser.parse_args() repo = Path(__file__).resolve().parents[2] with tempfile.TemporaryDirectory(prefix='raptor-periodic-isa-') as temporary: root = Path(temporary) for arch in ('arch-a', 'arch-b'): config = json.loads((repo / 'validation/pimsim_configs/pimcomp' / arch / 'throughput_config_1000ms.json').read_text()) config['chip_config']['core_cnt'] = 3 latency = {str(i): {str(j): int(i != j) for j in range(3)} for i in range(3)} for i in range(3): latency[str(i)]['-10'] = 2 latency['-10'] = {str(i): 2 for i in range(3)} network = root / 'network.json' network.write_text(json.dumps(dict(latency=latency))) config['chip_config']['network_config']['net_config_file_path'] = str(network) path = root / 'hardware.json' path.write_text(json.dumps(config)) hardware = config['chip_config']['core_config'] command = [args.diagnostic, str(root), f'--pim-target-config={path}', '--core-count=3', '--crossbar-size=128', f'--crossbar-count={hardware["matrix_config"]["xbar_array_count"]}'] def run(streams, *, issue=False, queues=True, valid=True, controller=False, oracle=False): for artifact in root.glob('core_*.pim'): artifact.unlink() (root / 'config.json').write_text(json.dumps(dict(core_cnt=len(streams), array_group_map={f'core{i}': [i + 1] for i in range(len(streams))}))) for i, instructions in enumerate(streams): (root / f'core_{i}.pim').write_bytes(struct.pack('<4sII', b'PIMB', 1, len(instructions)) + b''.join(instructions)) before = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in root.glob('core_*.pim')} result = subprocess.run(command + [f'--model-issue={int(issue)}', f'--model-network-queues={int(queues)}', f'--model-memory-controller={int(controller)}', f'--oracle-precision={int(oracle)}'], capture_output=True, text=True) assert (result.returncode == 0) == valid, result.stdout + result.stderr assert before == {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in root.glob('core_*.pim')} if not valid: return result report = json.loads(result.stdout) assert report['diagnostic_only'] and not report['selection_eligible'] assert report['status'] == 'estimated' if report['recurrence_lower_ns'] > 1: ratio = report['critical_cycle_delay_ns'] / report['critical_cycle_distance'] assert report['recurrence_lower_ns'] <= ratio <= report['recurrence_upper_ns'] return report bw = record(8, a=8, b=8) load = lambda n: record(25, length=n) store = lambda n: record(26, length=n) send = lambda peer: record(29, arg=peer, length=64) recv = lambda peer: record(30, arg=peer, length=64) sync = lambda peer, reg: record(32, arg=peer, a=reg) wait = lambda reg, n=1: record(31, a=reg, b=n) controller_fixtures = check_controller(run) native = run([[record(8, a=32, b=32), load(132), record(10, length=65), store(132)]], issue=True, controller=True, oracle=True) exported = run([[bw, load(33), record(10, length=65), store(33)]], issue=True, controller=True) for key in ('global_read_bytes', 'global_write_bytes', 'global_read_requests', 'global_write_requests', 'global_memory_service_ns'): assert native[key] == exported[key], key assert native['memory_controller'] == exported['memory_controller'] run([[record(8, a=16, b=16)]], oracle=True, valid=False) run([[load(3)]], oracle=True, valid=False) # One scan counts input, constant, and final-output accesses exactly. memory = run([[load(33), load(33), store(33)]]) assert memory['global_read_requests'] == 2 assert memory['global_read_bytes'] == 66 and memory['global_write_bytes'] == 33 assert memory['global_memory_service_ns'] == 260 assert memory['per_core'][0]['serial_work_ns'] == 348 folded = run([[load(66), store(33)]]) assert folded['global_memory_service_ns'] == 220 scalar = run([[record(1)] * 10], issue=True) assert scalar['per_core'][0]['serial_work_ns'] == 10 * hardware['period'] vector = run([[bw, record(10, length=65)]]) vector_cost = 2 * 2 * 20 + 3 * 4 + 2 * 30 assert vector['per_core'][0]['resource_service_ns'][1] == vector_cost issued = run([[bw, record(10, length=65)]], issue=True) assert issued['per_core'][0]['serial_work_ns'] == vector_cost + 2 * hardware['period'] omitted = run([[record(21, length=65), bw, record(21, length=65), record(10, length=65), record(21, length=65)]], issue=True) assert omitted['per_core'][0]['serial_work_ns'] == issued['per_core'][0]['serial_work_ns'] assert omitted['per_core'][0]['resource_service_ns'] == issued['per_core'][0]['resource_service_ns'] assert omitted['opcode_service_ns']['vsoftmax'] == 0 repeated = run([[bw] + [record(10, length=65)] * 10000]) assert repeated['per_core'][0]['serial_work_ns'] == 10000 * vector_cost assert repeated['events'] < 10 matrix = run([[bw, record(9, arg=2)], [bw, record(9, arg=2)]]) assert len(matrix['per_core']) == 2 assert matrix['per_core'][0]['instructions'] == matrix['per_core'][1]['instructions'] == 2 assert matrix['per_core'][0]['resource_service_ns'][0] > 0 assert matrix['per_core'][1]['serial_work_ns'] - matrix['per_core'][0]['serial_work_ns'] == 60 # Equal compute and traffic, different placement of a baseline load. reader = [bw, recv(0), record(9, arg=32), load(1024), send(0)] overlap = run([[send(1), load(1024), recv(1)], reader]) serial = run([[load(1024), send(1), recv(1)], reader]) assert overlap['global_memory_service_ns'] == serial['global_memory_service_ns'] assert overlap['interval_estimate_ns'] < serial['interval_estimate_ns'] output = run([[send(1)], [recv(0), store(64)]]) no_output = run([[send(1)], [recv(0)]]) assert output['global_write_requests'] == 1 and output['global_write_bytes'] == 64 assert output['direct_requests'] == 1 and output['direct_bytes'] == 64 assert output['interval_estimate_ns'] > no_output['interval_estimate_ns'] # Permission precedes the complete source read; enqueue is immediate. delayed = run([[send(1)], [record(27, length=128), recv(0)]]) graph, times = earliest_events(delayed) def event_id(core, pc, kind, occurrence=0): return [i for i, event in enumerate(graph['events']) if event == dict(core=core, pc=pc, kind=kind)][occurrence] request = event_id(0, 0, 'delivery', 0) permission = event_id(1, 1, 'delivery') enqueue = event_id(0, 0, 'payload-enqueue') delivery = event_id(0, 0, 'delivery', 1) complete = event_id(0, 0, 'send-complete') received = event_id(1, 1, 'receive-complete') assert times[permission] >= max(times[request], 60) + 1 assert times[enqueue] == times[permission] + 20 assert times[complete] == times[enqueue] assert times[delivery] == times[enqueue] + 2 assert times[received] == times[delivery] + 30 assert any(edge['source'] == delivery and edge['distance'] == 1 for edge in graph['dependencies']) # An occupied source FIFO delays request dispatch and permission. busy_routes = copy.deepcopy(latency) busy_routes['0']['2'] = 100 network.write_text(json.dumps(dict(latency=busy_routes))) graph, times = earliest_events(run([[sync(2, 0), send(1)], [recv(0)], [wait(0)]])) dispatch = event_id(0, 1, 'fifo-dispatch', 0) enqueue = event_id(0, 1, 'payload-enqueue') complete = event_id(0, 1, 'send-complete') assert times[dispatch] == 100 assert times[enqueue] == times[complete] == 122 network.write_text(json.dumps(dict(latency=latency))) pairs = run([[send(1), send(1)], [recv(0), recv(0)]]) assert pairs['direct_requests'] == 2 and pairs['direct_bytes'] == 128 interleaved = run([[wait(0), send(1), sync(1, 1)], [sync(0, 0), recv(0), wait(1)]]) assert interleaved['direct_requests'] == 1 # Exact-count handshakes repeat safely, including shared storage reuse. shared = run([[wait(0, 2), store(128), sync(1, 1), sync(2, 1)], [sync(0, 0), wait(1), load(128)], [sync(0, 0), wait(1), load(128)]]) assert shared['global_write_requests'] == 1 and shared['global_read_requests'] == 2 assert shared['global_write_bytes'] == 128 and shared['global_read_bytes'] == 256 stalled = run([[wait(0, 2), store(128), sync(1, 1), sync(2, 1)], [sync(0, 0), wait(1), load(128)], [bw, sync(0, 0), wait(1), load(128), record(10, length=4096)]]) assert stalled['interval_estimate_ns'] > shared['interval_estimate_ns'] notices = [[sync(1, 0), sync(1, 0), wait(1)], [wait(0, 2), sync(0, 1)]] queued = run(notices) unqueued = run(notices, queues=False) assert queued['interval_estimate_ns'] >= unqueued['interval_estimate_ns'] # Delivery starts when SYNC enqueues, before its local completion. slower_clock = copy.deepcopy(config) slower_clock['chip_config']['core_config']['period'] = 8 slower_clock['chip_config']['core_config']['local_memory_config'].pop('period') path.write_text(json.dumps(slower_clock)) signal = run([[sync(1, 0), wait(1)], [wait(0), sync(0, 1)]]) assert signal['interval_estimate_ns'] < 18 local = run([[record(27, length=65)]]) assert local['per_core'][0]['serial_work_ns'] == 60 independent_vector = run([[bw, record(10, length=65)]]) assert independent_vector['per_core'][0]['serial_work_ns'] == vector_cost different_memory_clock = copy.deepcopy(slower_clock) different_memory_clock['chip_config']['core_config']['local_memory_config']['period'] = 3 different_memory_clock['chip_config']['core_config']['vector_latency_cycle'] = 7 path.write_text(json.dumps(different_memory_clock)) changed_vector = run([[bw, record(10, length=65)]]) assert changed_vector['per_core'][0]['serial_work_ns'] == (vector_cost - 12) * 3 + 21 path.write_text(json.dumps(config)) zero_service = copy.deepcopy(config) zero_service['chip_config']['global_memory_config']['read_latency_cycle'] = 0 zero_routes = copy.deepcopy(latency) zero_routes['0']['-10'] = 0 zero_routes['0']['1'] = 100 network.write_text(json.dumps(dict(latency=zero_routes))) path.write_text(json.dumps(zero_service)) prefix = [[load(0), sync(1, 0), load(64), wait(1)], [wait(0), sync(0, 1)]] drained = run(prefix) undrained = run(prefix, queues=False) assert drained['interval_estimate_ns'] > undrained['interval_estimate_ns'] + 20 network.write_text(json.dumps(dict(latency=latency))) path.write_text(json.dumps(config)) if args.pimsim: # Scalar padding avoids the oracle's single-instruction fetch loop. # Golden pairs are (rounded graph estimate, oracle interval), in ns. # Their difference exposes clock-phase/frontend costs, without fitting the model. fixtures = [([[send(1)], [recv(0)]], {}, {}, (56, 57)), ([[send(1)], [record(27, length=1024), recv(0)]], {}, {}, (537, 539)), ([[send(1)], [recv(0)]], {'memory_period': 3}, {}, (157, 157)), ([[send(1)], [recv(0)]], {}, {'peer': 100}, (322, 321)), ([[sync(2, 0), send(1)], [recv(0)], [wait(0)]], {}, {'third': 100}, (125, 126)), ([[send(1), send(1)], [recv(0), recv(0)]], {}, {}, (111, 112)), ([[wait(0), send(1), sync(1, 1)], [sync(0, 0), recv(0), wait(1)]], {}, {}, (60, 65)), ([[bw, record(10, length=65)]], {'period': 8, 'memory_period': 3, 'vector': 7}, {}, (473, 488)), ([[bw, record(10, length=65)]], {'period': 5, 'memory_period': 2, 'vector': 11}, {}, (333, 340))] fixtures = [(*fixture, False) for fixture in fixtures] controller_expected = [(237, 243), (316, 320), (281, 280), (185, 186), (14145, 14169), (690, 698)] assert len(controller_fixtures) == len(controller_expected) fixtures += [(streams, {}, {}, expected, True) for streams, expected in zip(controller_fixtures, controller_expected)] for streams, clocks, links, expected, controller in fixtures: matched = copy.deepcopy(config) core = matched['chip_config']['core_config'] core['period'] = clocks.get('period', 1) core['local_memory_config']['period'] = clocks.get('memory_period', 1) core['vector_latency_cycle'] = clocks.get('vector', 4) matched['sim_config']['sim_time'] = 1 path.write_text(json.dumps(matched)) routes = copy.deepcopy(latency) routes['0']['1'] = links.get('peer', 1) routes['0']['2'] = links.get('third', 1) network.write_text(json.dumps(dict(latency=routes, energy={s: {d: 0 for d in row} for s, row in routes.items()}))) program = streams if controller else [[record(1)] + stream + [record(1)] for stream in streams] prediction = run(program, issue=True, controller=controller) result = subprocess.run([args.pimsim, str(root), str(path), '--gui=false', '--fast'], capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert 'Fast throughput converged' in result.stdout measured = float(re.search(r'average latency:\s+(\S+)', result.stdout)[1]) * 1e6 estimate = prediction['memory_controller']['interval_ns'] if controller else prediction['interval_estimate_ns'] assert (round(estimate), round(measured)) == expected # Omitting Softmax must leave timing, power, and energy unchanged. baseline = [bw, record(1), load(64), store(64), record(1)] omitted = [record(21, length=65)] + baseline[:2] + [record(21, length=65)] + baseline[2:] + [record(21, length=65)] predictions, measurements = [], [] def metrics(instructions, omitted_softmax=False): result = subprocess.run([args.pimsim, str(instructions), str(path), '--gui=false', '--fast'], capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stdout + result.stderr assert result.stderr.count('Warning: vsoftmax has no timing model;') == int(omitted_softmax) return tuple(float(re.search(rf'{key}:\s+(\S+)', result.stdout)[1]) for key in ('average latency', 'average power', 'average energy')) for index, program in enumerate((baseline, omitted)): prediction = run([program], issue=True, controller=True) predictions.append(prediction['memory_controller']['interval_ns']) measurements.append(metrics(root, omitted_softmax=bool(index))) assert predictions[0] == predictions[1] json_directory = root / 'softmax-json' json_directory.mkdir(exist_ok=True) json_program = json_directory / 'program.json' scalar_json = {'op': 'sldi', 'rd': 0, 'imm': 0} softmax_json = {'op': 'vsoftmax', 'rd': 0, 'rs1': 0, 'len': 65} json_program.write_text(json.dumps({'config': json.loads((root / 'config.json').read_text()), 'core0': [softmax_json, {'op': 'setbw', 'ibiw': 8, 'obiw': 8}, scalar_json, softmax_json, {'op': 'ld', 'rd': 0, 'rs1': 0, 'size': 64}, {'op': 'st', 'rd': 0, 'rs1': 0, 'size': 64}, scalar_json, softmax_json]})) measurements.append(metrics(json_program, omitted_softmax=True)) assert measurements[0] == measurements[1] == measurements[2] path.write_text(json.dumps(config)) network.write_text(json.dumps(dict(latency=latency))) run([[send(1), recv(1)], [send(0), recv(0)]], valid=False) run([[sync(1, 0)], [wait(0, 2)]], valid=False) run([[record(255, length=1)]], valid=False) run([[]], valid=False) unsupported = copy.deepcopy(config) unsupported['chip_config']['core_config']['rob_size'] = 2 path.write_text(json.dumps(unsupported)) run([[load(33)]], valid=False) if __name__ == '__main__': main()