338 lines
20 KiB
Python
338 lines
20 KiB
Python
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('<BBBBiiii', op, 0, 0, 0, arg, a, b, length)
|
|
|
|
|
|
def check_controller(run):
|
|
scalar = record(1)
|
|
load = lambda n: record(25, length=n)
|
|
store = lambda n: record(26, length=n)
|
|
work = record(27, length=256)
|
|
barrier = lambda peer: [record(32, arg=peer, a=0), record(31, a=0, b=1)]
|
|
staggered = [barrier(1) + [load(64), work], barrier(0) + [work, load(64)]]
|
|
burst = [barrier(1) + [work, load(64)], barrier(0) + [work, load(64)]]
|
|
a = run(staggered, controller=True, issue=True)
|
|
b = run(burst, controller=True, issue=True)
|
|
for report in (a, b):
|
|
assert report['memory_controller']['status'] == 'estimated'
|
|
assert report['global_read_bytes'] == 128
|
|
assert report['global_memory_service_ns'] == 160
|
|
assert a['memory_controller']['interval_ns'] < b['memory_controller']['interval_ns']
|
|
assert b['memory_controller'] == run(burst, controller=True, issue=True)['memory_controller']
|
|
mixed = run([[scalar, load(33), scalar], [scalar, store(128), scalar]],
|
|
controller=True, issue=True)
|
|
queued = mixed['memory_controller']
|
|
assert mixed['global_memory_service_ns'] == 280
|
|
assert queued['interval_ns'] >= 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']
|
|
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
|
|
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(21, 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()
|